要点
涉及的线程,有一个问题就是,函数在那个线程运行?有种
1.在发送信号的线程运行
2.在指定的线程运行,绑定的时候可以指定是在发信号的线程还是槽函数所在的线程
3.系统保证函数在合理的线程运行,就比如一个例子,我在子线程种发信号,其和这个信号绑定的槽函数没有指定线程,那么槽函数会运行在发送信号的线程;但是如果这个槽函数是更新UI的,而更新UI的函数是不允许运行在子线程中的;所以这里会自动完成线程的切换。
上面三种实际上是两种情况,一个是主动控制,如上面的1、2;一个是系统自动切换保护,如上面的3.
调查过程
1.qt 6.7 udp 通信
在 Qt 6.7 中实现 UDP 通信,你可以使用 Qt 的网络模块,特别是 QUdpSocket
类。这个类提供了发送和接收 UDP 数据报的功能。
下面是一个简单的例子,展示了如何使用 QUdpSocket
来发送和接收 UDP 数据。
1. 创建一个 UDP Socket
首先,你需要创建一个 QUdpSocket
对象,并连接它的信号到你的槽函数以处理接收到的数据。
#include <QUdpSocket>
#include <QCoreApplication>
#include <QDebug>
class UdpReceiver : public QObject {
Q_OBJECT
public:
UdpReceiver() {
socket = new QUdpSocket(this);
connect(socket, &QUdpSocket::readyRead, this, &UdpReceiver::readPendingDatagrams);
socket->bind(QHostAddress::Any, 1234);
}
private slots:
void readPendingDatagrams() {
while (socket->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(socket->pendingDatagramSize());
QHostAddress sender;
quint16 senderPort;
socket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
qDebug() << "Received:" << datagram << "from" << sender.toString() << ":" << senderPort;
}
}
private:
QUdpSocket *socket;
};
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
UdpReceiver receiver;
return a.exec();
}
#include "main.moc"
2. 发送 UDP 数据
你可以使用相同的 QUdpSocket
对象来发送数据。
void sendDatagram(const QByteArray &data, const QHostAddress &host, quint16 port) {
socket->writeDatagram(data, host, port);
}
确保在调用 sendDatagram
之前,socket 已经正确配置(不需要再次绑定,除非你需要改变端口)。
注意事项
- 确保你的防火墙设置允许 UDP 通信。
- UDP 是一种无连接的协议,发送的数据可能丢失、重复或乱序到达。
QUdpSocket
不保证数据报的到达,如果你需要可靠的数据传输,考虑使用 TCP。
这就是在 Qt 6.7 中使用 QUdpSocket
进行 UDP 通信的基本方法。根据你的具体需求,你可能需要对这个基本示例进行扩展或修改。
2.qt 6.7 udp 通信,在子线程运行
在 Qt 6.7 中,如果你想要让 UDP 通信在子线程中运行,你需要将 QUdpSocket
移动到一个新的线程中。这通常涉及到以下几个步骤:
- 创建一个新的类,该类包含
QUdpSocket
作为成员变量。 - 在这个类中实现 UDP 通信的逻辑。
- 创建一个
QThread
对象,并将你的类实例移动到这个线程中。 - 启动线程,并在需要时停止它。
下面是一个简单的例子,展示了如何在子线程中运行 UDP 通信:
#include <QCoreApplication>
#include <QDebug>
#include <QUdpSocket>
#include <QThread>
class UdpWorker : public QObject {
Q_OBJECT
public:
UdpWorker() {
socket = new QUdpSocket(this);
connect(socket, &QUdpSocket::readyRead, this, &UdpWorker::readPendingDatagrams);
}
void start(int port) {
socket->bind(QHostAddress::Any, port);
}
private slots:
void readPendingDatagrams() {
while (socket->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(socket->pendingDatagramSize());
QHostAddress sender;
quint16 senderPort;
socket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
qDebug() << "Received:" << datagram << "from" << sender.toString() << ":" << senderPort;
}
}
private:
QUdpSocket *socket;
};
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
QThread *thread = new QThread();
UdpWorker *worker = new UdpWorker();
worker->moveToThread(thread);
QObject::connect(thread, &QThread::started, worker, [worker]() {
worker->start(1234);
});
QObject::connect(worker, &UdpWorker::destroyed, thread, &QThread::quit);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
QObject::connect(worker, &UdpWorker::destroyed, worker, &UdpWorker::deleteLater);
thread->start();
return a.exec();
}
#include "main.moc"
在这个例子中,UdpWorker
类负责 UDP 通信。它有一个 QUdpSocket
成员变量,并在构造函数中连接了 readyRead
信号到 readPendingDatagrams
槽函数。start
方法用于绑定 socket 到指定的端口。
在 main
函数中,我们创建了一个 QThread
对象和一个 UdpWorker
对象。我们将 UdpWorker
对象移动到新创建的线程中,并连接了一些信号和槽来启动线程和清理资源。最后,我们调用 thread->start()
来启动线程。
这样,UDP 通信就会在子线程中运行,而不会阻塞主线程。
3.qt 6.7 对象所在的线程,是创建对象的时候的线程吗
在 Qt 6.7 中,对象所在的线程并不是由创建对象时的线程决定的,而是由对象被分配到的线程决定的。Qt 使用了对象树(object tree)来管理对象的生命周期和线程关联。当你创建一个 QObject 或其子类的实例时,这个实例默认与其父对象在同一个线程中。如果你没有指定父对象(即父对象为 nullptr),则对象将属于创建它的那个线程。
然而,你可以使用 moveToThread()
方法来改变对象所在的线程。当你调用这个方法并将对象移动到一个新的线程时,Qt 会确保对象的所有事件和信号/槽连接都在新的线程中处理。
因此,对象所在的线程是由你如何管理对象的生命周期和如何调用 moveToThread()
方法来决定的,而不是仅仅由创建对象时的线程决定的。
4.qt 6.7 udp 通信,在子线程运行,udp 通信对象继承QThread
在 Qt 中,通常不建议让通信对象(如 QUdpSocket
)直接继承 QThread
。更好的做法是使用 Qt 的信号和槽机制,在一个单独的类中封装 QUdpSocket
,然后将该类的一个实例移动到一个 QThread
中运行。
下面是一个示例,展示了如何在 Qt 6.7 中创建一个在子线程中运行的 UDP 通信对象:
#include <QCoreApplication>
#include <QDebug>
#include <QUdpSocket>
#include <QThread>
class UdpCommunicator : public QObject {
Q_OBJECT
public:
UdpCommunicator() {
socket = new QUdpSocket(this);
connect(socket, &QUdpSocket::readyRead, this, &UdpCommunicator::readPendingDatagrams);
}
void start(int port) {
if (socket->bind(QHostAddress::Any, port)) {
qDebug() << "UDP server started on port" << port;
} else {
qDebug() << "UDP server failed to start on port" << port;
}
}
void stop() {
socket->close();
}
private slots:
void readPendingDatagrams() {
while (socket->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(socket->pendingDatagramSize());
QHostAddress sender;
quint16 senderPort;
socket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
qDebug() << "Received:" << datagram << "from" << sender.toString() << ":" << senderPort;
}
}
private:
QUdpSocket *socket;
};
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
QThread *thread = new QThread();
UdpCommunicator *communicator = new UdpCommunicator();
communicator->moveToThread(thread);
QObject::connect(thread, &QThread::started, communicator, &UdpCommunicator::start);
QObject::connect(communicator, &UdpCommunicator::destroyed, thread, &QThread::quit);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
QObject::connect(communicator, &UdpCommunicator::destroyed, communicator, &UdpCommunicator::deleteLater);
thread->start();
// Keep the application running for a while to receive datagrams
QTimer::singleShot(10000, &a, &QCoreApplication::quit);
return a.exec();
}
#include "main.moc"
在这个例子中,UdpCommunicator
类封装了 QUdpSocket
,并提供了 start
和 stop
方法来控制 UDP 通信。我们将 UdpCommunicator
的一个实例移动到了一个单独的 QThread
中,并通过信号和槽来启动和停止它。
注意,我们在 main
函数中使用了一个 QTimer::singleShot
来在一段时间后退出应用程序,以便有足够的时间接收 UDP 数据报。在实际应用中,你可能需要实现更复杂的逻辑来控制应用程序的退出。
5.上面的例子,修改成让UdpCommunicator直接继承QThread
虽然通常不建议让通信对象直接继承 QThread
,但如果你确实想要这样做,下面是一个修改后的例子,其中 UdpCommunicator
直接继承自 QThread
:
#include <QCoreApplication>
#include <QDebug>
#include <QUdpSocket>
#include <QThread>
class UdpCommunicator : public QThread {
Q_OBJECT
public:
UdpCommunicator(QObject *parent = nullptr) : QThread(parent) {
socket = new QUdpSocket(this);
connect(socket, &QUdpSocket::readyRead, this, &UdpCommunicator::readPendingDatagrams);
}
~UdpCommunicator() {
socket->deleteLater();
}
void run() override {
int port = 1234; // 示例端口号
if (socket->bind(QHostAddress::Any, port)) {
qDebug() << "UDP server started on port" << port;
} else {
qDebug() << "UDP server failed to start on port" << port;
return;
}
exec(); // 进入事件循环
}
void stop() {
socket->close();
quit(); // 退出事件循环
wait(); // 等待线程结束
}
private slots:
void readPendingDatagrams() {
while (socket->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(socket->pendingDatagramSize());
QHostAddress sender;
quint16 senderPort;
socket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
qDebug() << "Received:" << datagram << "from" << sender.toString() << ":" << senderPort;
}
}
private:
QUdpSocket *socket;
};
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
UdpCommunicator *communicator = new UdpCommunicator();
QObject::connect(&a, &QCoreApplication::aboutToQuit, communicator, &UdpCommunicator::stop);
communicator->start(); // 启动线程
return a.exec();
}
#include "main.moc"
在这个例子中,UdpCommunicator
类直接继承自 QThread
,并重写了 run
方法来启动 UDP 服务器。我们还添加了一个 stop
方法来关闭 socket 并退出线程的事件循环。在 main
函数中,我们创建了一个 UdpCommunicator
的实例,并在应用程序即将退出时连接了 aboutToQuit
信号到 stop
方法,以确保线程能够正确清理并退出。
6.为什么不建议直接继承呢
不建议直接继承 QThread
来创建线程并处理业务逻辑,主要是基于以下几个原因:
1. 线程管理与业务逻辑耦合
当 QThread
被直接继承以执行特定任务时,线程的管理(如启动、停止、暂停等)和业务逻辑被紧密地耦合在一起。这导致代码结构不够清晰,维护和扩展变得困难。理想情况下,线程的管理和具体的业务逻辑应该分离,以提高代码的可读性和可维护性。
2. 事件循环的复杂性
QThread
的 run
方法默认并不启动事件循环(在早期的 Qt 版本中可能需要手动调用 exec()
)。如果业务逻辑依赖于 Qt 的信号和槽机制,或者需要处理定时器、网络事件等,那么直接继承 QThread
可能不足以满足需求。此外,手动管理事件循环也增加了代码的复杂性。
3. 更好的替代方案
Qt 提供了更灵活和强大的线程处理机制,如使用 QObject::moveToThread()
方法将对象移动到不同的线程中。这种方法允许你将业务逻辑封装在一个或多个 QObject
子类中,然后通过信号和槽机制在不同线程之间进行通信。这种方式不仅代码结构清晰,而且能够充分利用 Qt 的事件驱动特性。
4. 官方推荐
Qt 官方文档和许多经验丰富的开发者都推荐使用 QObject::moveToThread()
方法来处理多线程问题,而不是直接继承 QThread
。这是因为这种方式更符合 Qt 的设计哲学,即利用信号和槽机制来简化跨线程通信。
5. 线程安全问题
直接继承 QThread
并在其中处理复杂的业务逻辑时,需要特别注意线程安全问题。例如,当多个线程访问共享资源时,必须使用互斥锁(如 QMutex
)或其他同步机制来避免数据竞争和条件竞争等问题。而使用 QObject::moveToThread()
方法时,由于业务逻辑和线程管理是分离的,因此更容易实现线程安全。
综上所述,虽然直接继承 QThread
可以实现多线程功能,但出于代码结构、事件循环管理、替代方案的优越性、官方推荐以及线程安全等方面的考虑,通常不建议采用这种方式。相反,使用 QObject::moveToThread()
方法来处理多线程问题是一种更加优雅和强大的解决方案。
7.上面的例子中,将socket变成通过构造函数传入的对象,在主线程中创建
为了将 QUdpSocket
对象通过构造函数传入 UdpCommunicator
类,并在主线程中创建该对象,我们需要对代码进行一些调整。以下是修改后的代码示例:
#include <QCoreApplication>
#include <QDebug>
#include <QUdpSocket>
#include <QThread>
class UdpCommunicator : public QThread {
Q_OBJECT
public:
UdpCommunicator(QUdpSocket *socket, QObject *parent = nullptr)
: QThread(parent), m_socket(socket) {
connect(m_socket, &QUdpSocket::readyRead, this, &UdpCommunicator::readPendingDatagrams);
}
~UdpCommunicator() {
// 注意:这里不再删除 socket,因为它是由外部传入的
m_socket = nullptr;
}
void run() override {
// 假设 socket 已经在外部绑定到了某个端口
exec(); // 进入事件循环
}
void stop() {
m_socket->close();
quit(); // 退出事件循环
wait(); // 等待线程结束
}
private slots:
void readPendingDatagrams() {
while (m_socket->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(m_socket->pendingDatagramSize());
QHostAddress sender;
quint16 senderPort;
m_socket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
qDebug() << "Received:" << datagram << "from" << sender.toString() << ":" << senderPort;
}
}
private:
QUdpSocket *m_socket;
};
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
QUdpSocket *socket = new QUdpSocket();
int port = 1234;
if (socket->bind(QHostAddress::Any, port)) {
qDebug() << "UDP server started on port" << port;
} else {
qDebug() << "UDP server failed to start on port" << port;
return 1;
}
UdpCommunicator *communicator = new UdpCommunicator(socket);
QObject::connect(&a, &QCoreApplication::aboutToQuit, communicator, &UdpCommunicator::stop);
communicator->start(); // 启动线程
return a.exec();
}
#include "main.moc"
在这个修改后的例子中,QUdpSocket
对象是在 main
函数中创建的,并通过构造函数传入 UdpCommunicator
类。这样,UdpCommunicator
类就不再负责创建和管理 QUdpSocket
对象,而是只负责处理 UDP 数据报的读取和线程的事件循环。当应用程序即将退出时,aboutToQuit
信号会触发 stop
方法,从而关闭 socket 并退出线程的事件循环。
上面的例子中,将socket变成通过构造函数传入的对象,在主线程中创建
为了将 QUdpSocket
对象通过构造函数传入 UdpCommunicator
类,并在主线程中创建该对象,我们需要对代码进行一些调整。以下是修改后的代码示例:
#include <QCoreApplication>
#include <QDebug>
#include <QUdpSocket>
#include <QThread>
class UdpCommunicator : public QThread {
Q_OBJECT
public:
UdpCommunicator(QUdpSocket *socket, QObject *parent = nullptr)
: QThread(parent), m_socket(socket) {
connect(m_socket, &QUdpSocket::readyRead, this, &UdpCommunicator::readPendingDatagrams);
}
~UdpCommunicator() {
// 注意:这里不再删除 socket,因为它是由外部传入的
m_socket = nullptr;
}
void run() override {
// 假设 socket 已经在外部绑定到了某个端口
exec(); // 进入事件循环
}
void stop() {
m_socket->close();
quit(); // 退出事件循环
wait(); // 等待线程结束
}
private slots:
void readPendingDatagrams() {
while (m_socket->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(m_socket->pendingDatagramSize());
QHostAddress sender;
quint16 senderPort;
m_socket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
qDebug() << "Received:" << datagram << "from" << sender.toString() << ":" << senderPort;
}
}
private:
QUdpSocket *m_socket;
};
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
QUdpSocket *socket = new QUdpSocket();
int port = 1234;
if (socket->bind(QHostAddress::Any, port)) {
qDebug() << "UDP server started on port" << port;
} else {
qDebug() << "UDP server failed to start on port" << port;
return 1;
}
UdpCommunicator *communicator = new UdpCommunicator(socket);
QObject::connect(&a, &QCoreApplication::aboutToQuit, communicator, &UdpCommunicator::stop);
communicator->start(); // 启动线程
return a.exec();
}
#include "main.moc"
在这个修改后的例子中,QUdpSocket
对象是在 main
函数中创建的,并通过构造函数传入 UdpCommunicator
类。这样,UdpCommunicator
类就不再负责创建和管理 QUdpSocket
对象,而是只负责处理 UDP 数据报的读取和线程的事件循环。当应用程序即将退出时,aboutToQuit
信号会触发 stop
方法,从而关闭 socket 并退出线程的事件循环。
8.为UdpCommunicator 添加一个给m_socket写入数据的接口
为了给 UdpCommunicator
类添加一个向 m_socket
写入数据的接口,我们可以在类中定义一个公共方法,该方法接受要发送的数据、目标地址和端口号作为参数,并使用 QUdpSocket
的 writeDatagram
方法来发送数据。以下是修改后的 UdpCommunicator
类,包含了新的 sendDatagram
方法:
#include <QCoreApplication>
#include <QDebug>
#include <QUdpSocket>
#include <QThread>
class UdpCommunicator : public QThread {
Q_OBJECT
public:
UdpCommunicator(QUdpSocket *socket, QObject *parent = nullptr)
: QThread(parent), m_socket(socket) {
connect(m_socket, &QUdpSocket::readyRead, this, &UdpCommunicator::readPendingDatagrams);
}
~UdpCommunicator() {
m_socket = nullptr;
}
void run() override {
exec(); // 进入事件循环
}
void stop() {
m_socket->close();
quit(); // 退出事件循环
wait(); // 等待线程结束
}
// 新增的发送数据接口
void sendDatagram(const QByteArray &data, const QHostAddress &address, quint16 port) {
m_socket->writeDatagram(data, address, port);
}
private slots:
void readPendingDatagrams() {
while (m_socket->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(m_socket->pendingDatagramSize());
QHostAddress sender;
quint16 senderPort;
m_socket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
qDebug() << "Received:" << datagram << "from" << sender.toString() << ":" << senderPort;
}
}
private:
QUdpSocket *m_socket;
};
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
QUdpSocket *socket = new QUdpSocket();
int port = 1234;
if (socket->bind(QHostAddress::Any, port)) {
qDebug() << "UDP server started on port" << port;
} else {
qDebug() << "UDP server failed to start on port" << port;
return 1;
}
UdpCommunicator *communicator = new UdpCommunicator(socket);
QObject::connect(&a, &QCoreApplication::aboutToQuit, communicator, &UdpCommunicator::stop);
communicator->start(); // 启动线程
// 示例:发送数据
QByteArray data = "Hello, UDP!";
QHostAddress address("127.0.0.1");
quint16 remotePort = 1234;
communicator->sendDatagram(data, address, remotePort);
return a.exec();
}
#include "main.moc"
在这个例子中,sendDatagram
方法允许外部调用者向指定的地址和端口发送 UDP 数据报。在 main
函数中,我们添加了一个示例,展示了如何使用这个新接口来发送数据。
9.如果我在主线程中,调用sendDatagram接口,处理sendDatagram接口调用引发的回复数据的readPendingDatagrams接口在子线程运行,还是在主线程运行
在您的场景中,sendDatagram
接口是在 UdpCommunicator
类中定义的,而这个类是从 QThread
派生的。当您在主线程中调用 sendDatagram
接口时,实际上是在向 m_socket
发送数据,而这个 m_socket
是与 UdpCommunicator
线程的事件循环相关联的。
readPendingDatagrams
是一个槽函数,它连接到了 m_socket
的 readyRead
信号。当 m_socket
接收到数据时,readyRead
信号会在 m_socket
所属的线程(在这种情况下是 UdpCommunicator
线程)中被触发,因此 readPendingDatagrams
槽函数也会在 UdpCommunicator
线程中执行。
总结一下,即使您在主线程中调用 sendDatagram
接口,处理接收到的回复数据的 readPendingDatagrams
接口也会在 UdpCommunicator
线程(子线程)中运行,而不是在主线程中运行。这是因为 QUdpSocket
的信号和槽机制确保了相关的槽函数在正确的线程中被调用。
10.qt 允许在子线程中调用UI控件的接口吗
在Qt中,直接在子线程中调用UI控件的接口是不允许的,也是不安全的。Qt的UI控件(如QWidget及其子类)不是线程安全的,它们只能在创建它们的线程(通常是主线程)中进行操作。
如果您需要在子线程中更新UI,您应该使用信号和槽机制,将需要更新的信息从子线程发送到主线程,然后在主线程中更新UI。这是Qt推荐的做法,也是线程间通信的常用方式。
例如,您可以在子线程中发射一个自定义信号,该信号携带需要更新的数据。然后,在主线程中连接这个信号到一个槽函数,该槽函数负责更新UI。这样,即使信号是在子线程中发射的,槽函数也会在主线程中被调用,从而安全地更新UI。
11.能对这种情况做一个实验,且提供一版代码吗
当然可以。以下是一个简单的Qt实验,演示了如何从子线程发送信号以在主线程中更新UI。
首先,创建一个新的Qt Widgets Application项目,并在其中添加以下代码:
main.cpp
#include <QApplication>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QThread>
#include <QDebug>
class Worker : public QObject {
Q_OBJECT
public:
Worker() {}
signals:
void updateButtonLabel(const QString &text);
public slots:
void doWork() {
// 模拟一些耗时的工作
QThread::sleep(2);
emit updateButtonLabel("Updated from Worker");
}
};
class MainWindow : public QWidget {
Q_OBJECT
public:
MainWindow() {
QPushButton *button = new QPushButton("Click me", this);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(button);
Worker *worker = new Worker();
QThread *thread = new QThread();
worker->moveToThread(thread);
connect(button, &QPushButton::clicked, worker, &Worker::doWork);
connect(worker, &Worker::updateButtonLabel, button, &QPushButton::setText);
connect(thread, &QThread::started, worker, &Worker::doWork);
connect(worker, &Worker::updateButtonLabel, thread, &QThread::quit);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(worker, &Worker::updateButtonLabel, worker, &Worker::deleteLater);
thread->start();
}
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
MainWindow window;
window.show();
return app.exec();
}
#include "main.moc"
在这个例子中,Worker
类是一个在子线程中执行的对象。它有一个 doWork
槽函数,该函数在调用时会休眠2秒,然后发射 updateButtonLabel
信号。
MainWindow
类是主窗口类,它包含一个按钮。当按钮被点击时,它会触发 Worker
的 doWork
槽函数。Worker
完成工作后,会发射 updateButtonLabel
信号,该信号连接到按钮的 setText
槽函数,从而更新按钮的文本。
注意,我们使用了 QThread
来创建一个子线程,并将 Worker
对象移动到了这个子线程中。我们还连接了 QThread
的 started
信号到 Worker
的 doWork
槽函数,以便在子线程启动时开始工作。
运行这个程序,点击按钮,你会看到按钮的文本在2秒后更新为 "Updated from Worker",这表明信号和槽机制正确地跨线程工作了。
12 请追加线程ID输出的逻辑,已确认函数当时执行所在的线程
为了确认函数当时执行所在的线程,我们可以在关键位置输出线程的ID。在Qt中,可以通过QThread::currentThreadId()
来获取当前线程的ID。下面是在之前的示例代码中追加线程ID输出逻辑的版本:
main.cpp
#include <QApplication>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QThread>
#include <QDebug>
class Worker : public QObject {
Q_OBJECT
public:
Worker() {}
signals:
void updateButtonLabel(const QString &text);
public slots:
void doWork() {
// 输出当前线程的ID
qDebug() << "Worker thread ID:" << QThread::currentThreadId();
// 模拟一些耗时的工作
QThread::sleep(2);
emit updateButtonLabel("Updated from Worker");
}
};
class MainWindow : public QWidget {
Q_OBJECT
public:
MainWindow() {
QPushButton *button = new QPushButton("Click me", this);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(button);
Worker *worker = new Worker();
QThread *thread = new QThread();
worker->moveToThread(thread);
connect(button, &QPushButton::clicked, worker, &Worker::doWork);
connect(worker, &Worker::updateButtonLabel, button, &QPushButton::setText);
connect(thread, &QThread::started, worker, &Worker::doWork);
connect(worker, &Worker::updateButtonLabel, thread, &QThread::quit);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(worker, &Worker::updateButtonLabel, worker, &Worker::deleteLater);
// 输出主线程的ID
qDebug() << "Main thread ID:" << QThread::currentThreadId();
thread->start();
}
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
MainWindow window;
window.show();
return app.exec();
}
#include "main.moc"
在这个修改后的版本中,我们在Worker
类的doWork
槽函数中输出了当前线程的ID,以确认这个函数是在子线程中执行的。同时,在MainWindow
的构造函数中,我们也输出了主线程的ID,以便进行比较。
运行这个程序,并点击按钮,你将在控制台看到类似以下的输出:
Main thread ID: 0x1234abcd
Worker thread ID: 0x5678efgh
这里的ID是示例性的,实际运行时你将看到不同的值。这个输出确认了doWork
函数是在子线程中执行的,而主窗口的构造函数是在主线程中执行的。
13 QThread::quit
在Qt中,QThread::quit()
函数是一个槽函数,它的作用是告诉线程的事件循环结束运行。当你调用这个函数时,如果线程正在运行一个事件循环(即调用了exec()
),那么这个事件循环将会退出,线程随之也会结束。
重要的是要理解,QThread::quit()
并不会立即终止线程。它只是请求事件循环退出,然后线程会在处理完所有剩余的事件后优雅地结束。如果你需要立即终止线程,应该使用更激进的方法,比如QThread::terminate()
,但这种方法是不安全的,因为它会立即停止线程,可能导致资源泄露或数据不一致。
在大多数情况下,你应该避免使用QThread::terminate()
,而是使用QThread::quit()
来请求线程优雅地退出。然后,你可以使用QThread::wait()
来等待线程实际结束。
这里是一个使用QThread::quit()
的例子:
class Worker : public QObject {
Q_OBJECT
public slots:
void doWork() {
// ... 执行一些工作 ...
// 现在工作完成了,告诉线程可以退出了
QThread::currentThread()->quit();
}
};
// 在另一个地方
QThread *thread = new QThread();
Worker *worker = new Worker();
worker->moveToThread(thread);
// 连接信号和槽来开始工作
connect(someSignalSource, &SomeClass::someSignal, worker, &Worker::doWork);
// 连接线程的finished信号来清理
connect(thread, &QThread::finished, worker, &Worker::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
// 开始线程的事件循环
thread->start();
// ... 在某个时刻,someSignal会被发射,导致worker的doWork槽函数被调用 ...
// 当doWork槽函数调用QThread::currentThread()->quit()时,线程的事件循环会结束,
// 然后发出finished信号,导致worker和thread对象被删除。
在这个例子中,当doWork
槽函数完成时,它会调用QThread::currentThread()->quit()
来请求线程的事件循环退出。然后,线程会发出finished
信号,这个信号被用来清理worker
和thread
对象。
14.QThread::deleteLater
QThread::deleteLater()
是 Qt 框架中的一个成员函数,它属于 QObject
类,而 QThread
继承自 QObject
。当你调用 deleteLater()
时,你并不是立即删除对象,而是将对象标记为“待删除”。这意味着在对象的线程的事件循环下一次迭代时,或者在对象所在的线程结束时(如果对象没有事件循环),对象将被删除。
在 QThread
的上下文中,deleteLater()
非常有用,因为你不应该直接从另一个线程删除 QThread
对象或其子对象。这样做可能会导致未定义的行为或程序崩溃,因为 Qt 的对象模型不是线程安全的。
使用 deleteLater()
可以确保对象在正确的线程中被删除,即对象所在的线程。这是通过内部的事件循环和事件处理机制来实现的。当对象被标记为待删除时,它会在适当的时候收到一个删除事件,然后在其线程上下文中被安全地删除。
这里是一个使用 QThread::deleteLater()
的例子:
class Worker : public QObject {
Q_OBJECT
public slots:
void doWork() {
// ... 执行一些工作 ...
// 工作完成,准备退出线程
emit finished();
}
signals:
void finished();
};
// 在另一个地方
QThread *thread = new QThread();
Worker *worker = new Worker();
worker->moveToThread(thread);
// 连接信号和槽来开始和结束工作
connect(someSignalSource, &SomeClass::someSignal, worker, &Worker::doWork);
connect(worker, &Worker::finished, thread, &QThread::quit);
connect(worker, &Worker::finished, worker, &Worker::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
// 开始线程的事件循环
thread->start();
// ... 在某个时刻,someSignal 会被发射,导致 worker 的 doWork 槽函数被调用 ...
// 当 doWork 槽函数发出 finished 信号时,它会导致线程退出(通过调用 QThread::quit)
// 并且标记 worker 和 thread 对象为待删除(通过调用 deleteLater)
// 当线程的事件循环结束时,这些对象将被删除
在这个例子中,当 Worker
类的 doWork
槽函数完成时,它会发出一个 finished
信号。这个信号被连接到 QThread::quit
,以请求线程的事件循环退出,同时也被连接到 Worker
对象的 deleteLater
,以标记该对象为待删除。最后,当线程的事件循环结束时,QThread
对象也会被删除。
15.QThread 常用接口
QThread
是 Qt 框架中用于管理线程的类。它提供了一系列常用的接口,使得在多线程编程中能够更加方便地创建、启动、停止和管理线程。以下是一些 QThread
的常用接口:
- 构造函数和析构函数:
QThread(QObject *parent = nullptr)
:构造函数,创建一个新的线程对象。~QThread()
:析构函数,销毁线程对象。注意,在调用析构函数之前,应确保线程已经停止运行。
- 线程启动和终止:
void start(Priority priority = InheritPriority)
:启动线程。可以通过参数指定线程的优先级。void quit()
:请求线程的事件循环退出。如果线程正在运行事件循环,它将优雅地结束。void terminate()
:强制终止线程。这是一个不安全的操作,因为它不会等待线程优雅地结束,可能会导致资源泄露或数据不一致。
- 线程状态查询:
bool isRunning()
:返回线程是否正在运行。bool isFinished()
:返回线程是否已经结束。
- 线程等待:
bool wait(unsigned long time = ULONG_MAX)
:等待线程结束。可以通过参数指定等待的超时时间。
- 线程ID和优先级:
QThread::currentThreadId()
:返回当前线程的ID。QThread::currentThread()
:返回当前线程的指针。Priority priority()
:返回线程的优先级。void setPriority(Priority priority)
:设置线程的优先级。
- 事件循环:
int exec()
:启动线程的事件循环。在调用此函数后,线程将开始处理事件,直到调用quit()
或terminate()
。
- 信号和槽:
void started()
:当线程开始运行时发射此信号。void finished()
:当线程结束时发射此信号。
使用 QThread
时,通常的做法是将工作类的实例移动到一个新的线程中,而不是直接重写 QThread
的 run()
方法。这样可以保持 QThread
的简洁性,并将线程逻辑与工作逻辑分离。工作类应该是一个继承自 QObject
的类,并且实现所需的信号和槽来进行线程间的通信。
16.QUdpSocket 常用接口
QUdpSocket
是 Qt 框架中用于 UDP 通信的类,它继承自 QAbstractSocket
,提供了发送和接收 UDP 数据报的功能。以下是 QUdpSocket
的一些常用接口及其简要介绍:
构造函数和析构函数
QUdpSocket(QObject *parent = nullptr)
:构造函数,创建一个新的QUdpSocket
对象,可以选择性地指定父对象。~QUdpSocket()
:析构函数,销毁QUdpSocket
对象。在析构过程中,如果套接字仍然打开,它会自动关闭。
绑定和解绑
bool bind(quint16 port = 0, BindMode mode = DefaultForPlatform)
:绑定套接字到指定的端口上。如果端口为0,则系统会自动选择一个可用的端口。绑定成功后,套接字进入监听状态。void abort()
:立即中止当前操作,并关闭套接字。这不同于close()
,abort()
会丢弃写入缓冲区中的所有待发送数据。
发送数据报
qint64 writeDatagram(const QByteArray &datagram, const QHostAddress &host, quint16 port)
:向指定的主机和端口发送数据报。返回成功发送的字节数,如果发送失败则返回-1。qint64 writeDatagram(const char *data, qint64 size, const QHostAddress &address, quint16 port)
:这是另一个重载版本,允许直接发送字符数组形式的数据报。
接收数据报
qint64 readDatagram(char *data, qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr)
:从套接字接收一个数据报,并将其存储在提供的缓冲区中。如果提供了address
和port
参数,它们将被填充为发送方的地址和端口。QByteArray receiveDatagram(qint64 maxSize = INT_MAX, QHostAddress *address = nullptr, quint16 *port = nullptr)
:接收一个数据报,并将其作为QByteArray
返回。如果提供了address
和port
参数,它们将被填充为发送方的地址和端口。
状态和错误处理
QAbstractSocket::SocketState state() const
:返回套接字的当前状态,如未连接、监听、已连接等。bool hasPendingDatagrams() const
:检查是否有挂起的数据报等待读取。qint64 pendingDatagramSize() const
:返回第一个挂起的数据报的大小(以字节为单位)。void readyRead()
:当套接字有数据可读时,会发出此信号。通常,在槽函数中连接此信号以处理接收到的数据。void errorOccurred(QAbstractSocket::SocketError socketError)
:当发生错误时,会发出此信号,并携带错误类型的信息。
多播组管理
bool joinMulticastGroup(const QHostAddress &groupAddress)
:加入指定的多播组。成功加入后,该套接字将能够接收发送到该多播组的数据报。bool leaveMulticastGroup(const QHostAddress &groupAddress)
:离开指定的多播组。离开后,该套接字将不再接收发送到该多播组的数据报。
这些接口提供了使用 QUdpSocket
进行 UDP 通信所需的基本功能。通过组合使用这些接口,可以实现复杂的网络通信逻辑。