QT系统篇(5)(下)
一、多线程
1.了解
在Qt中,多线程的处理一般是通过QThread类来实现。QThread代表一个在应用程序中可以独立控制的线程,也可以和进程中的其他线程共享数据。QThread对象管理程序中的一个控制线程。
2.方法
3.方法
wait
bool QThread::wait( unsigned long time = ULONG_MAX ); // 功能: // 阻塞当前线程,直到该 QThread 对象所代表的线程执行完毕(退出),或达到指定的超时时间 // 参数说明: // unsigned long time = ULONG_MAX: // 等待的超时时间(单位:毫秒) // - 若 time == ULONG_MAX(默认):无限等待,直到目标线程退出 // - 若 time > 0:最多等待 time 毫秒,超时后返回 false // 返回值: // bool: // - 若目标线程已正常退出,则返回 true // - 若超时(time 指定的毫秒数内线程未退出),则返回 false // - 注意:如果目标线程尚未启动(未调用 start())或已经退出,调用 wait() 也会立即返回 true4.代码
//thread.h class Thread : public QThread { Q_OBJECT public: Thread(); void run();//重写run函数 public: static int num;//统计数量 static QMutex mutex; }; //thread.cpp int Thread::num = 0; QMutex Thread::mutex; Thread::Thread() {} void Thread::run() { for(int i = 0;i < 10;i++) { // mutex.lock();//上锁 // num++; // mutex.unlock();//解锁 QMutexLocker locker(&mutex);//上锁,当这个对象销毁后会自动解锁 num++; } } //.cpp Thread t1; Thread t2; //启动线程 t1.start(); t2.start(); //阻塞主线程,等待t1线程和t2线程返回 t1.wait(); t2.wait(); qDebug()<<Thread::num;5.要点
QMutexLocker是 Qt 中的智能锁,用于自动管理互斥量的加锁和解锁。
Qt使用多线程的意义在于,将IO 操作隔离出去,即保证主线程与上传/下载等操作隔离。例如游戏一边更新还能一边运行,就是因为更新(IO 操作)放在后台线程,主线程仍可处理界面和交互。
服务器使用多线程,是为了充分利用 CPU 资源(避免单线程下 I/O 等待导致的 CPU 空闲)。
在Qt中,工作线程不允许直接操作UI界面,否则会报错;而是通过信号槽的方式(将数据发送至主线程)间接操作UI。
6.connect拓展
connect()函数的第五个参数为Qt::ConnectionType,用于指定信号和槽的连接类型,同时影响信号的传递方式和槽函数的执行顺序。Qt::ConnectionType提供了以下五种方式
1.1 线程安全
1.互斥锁的种类
QMutex、QMutexLocker
2.互斥锁的作用
互斥锁是一种保护和防止多个线程同时访问同一对象实例的方法。在 Qt 中,互斥锁主要是通过QMutex类来处理。
3.QMutex和QMutexLocker的区别
QMutex:
特点:QMutex是 Qt 框架提供的互斥锁类,用于保护共享资源的访问,实现线程间的互斥操作。
用途:在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。
QMutex mutex; mutex.lock(); //上锁 //访问共享资源 //... mutex.unlock(); //解锁QMutexLocker:
特点:QMutexLocker是 QMutex 的辅助类,使用RAII(Resource Acquisition Is Initialization)方式对互斥锁进行加锁和解锁操作。
用途:简化对互斥锁的加锁和解锁操作,避免忘记解锁导致的死锁等问题。
QMutex mutex; { QMutexLocker locker(&mutex); //在作⽤域内⾃动上锁 //访问共享资源 //... }//在作⽤域结束时⾃动解锁
4.其他锁
QReadLocker(共享锁)
用于读操作上锁,允许多个线程同时读取共享资源,但不能写入。
QWriteLocker(独占锁,和正常锁没区别)
用于写操作上锁,只允许一个线程写入共享资源,并且禁止其他线程读取或写入。
1.2 条件变量
1.了解
在多线程编程中,除了需要等待操作系统调度之外,有时一个线程还必须等待某个特定条件满足才能继续执行,这就带来了同步问题。通常的解决思路是:该线程先释放互斥锁(或读写锁),然后进入睡眠状态,让其他线程得以运行。当条件满足时,另一个线程会将其唤醒。
在 Qt 中,专门提供了QWaitCondition类来解决像上述这样的问题。
特点:QWaitCondition是 Qt 框架提供的条件变量类,用于线程之间的消息通信和同步。
用途:在某个条件满足时等待或唤醒线程,用于线程的同步和协调。
2.代码
//.h class MyWorker : public QThread { Q_OBJECT public: explicit MyWorker(QObject *parent = nullptr); // 由主线程调用,用于唤醒等待中的工作线程 void trigger(); // 线程入口函数,在其中等待条件满足 void run(); private: QMutex m_mutex;//锁 QWaitCondition m_cond;//条件变量 bool m_ready;// 条件标志 }; //.cpp void MyWorker::run() { qDebug() << "Worker: 等待条件..."; m_mutex.lock(); while (!m_ready) { // 条件不满足,线程进入睡眠等待,同时自动释放 m_mutex,唤醒后又会自动申请锁 m_cond.wait(&m_mutex); } m_mutex.unlock();//给没有睡眠的或者唤醒后的进行解锁 qDebug() << "Worker: 条件满足,继续执行"; } void MyWorker::trigger() { m_mutex.lock(); m_ready = true; //改变条件 m_cond.wakeOne(); //唤醒一个等待的线程 m_mutex.unlock(); }3.要点
线程进入休眠会自动释放锁,唤醒后又会自动重新申请锁,然后从当前位置开始执行代码
使用
while而不是if的原因主要有两点:
虚假唤醒:操作系统可能无故唤醒等待的线程,此时条件并未满足,需要重新等待。
多线程竞争:当线程被唤醒并重新获取锁之前,其他线程可能已经改变了条件,导致条件再次不满足。
补充:为什么使用
while而不是if?因为如果使用if,被唤醒后肯定if条件不满足,但代码不会执行else if或else分支(即不会重新检查条件)。此外,还存在一种情况:线程被唤醒后可能没有立即申请到锁(需要继续等待锁到位),而在等待锁的过程中,条件可能再次发生变化。因此必须使用while循环反复检查条件,确保在条件真正满足时才继续执行。
4.方法
wakeOne
#include <QWaitCondition> void QWaitCondition::wakeOne(); // 功能: // 唤醒一个正在等待该 QWaitCondition 的线程 // 如果有多个线程在等待,则随机选择一个线程唤醒 // 被唤醒的线程会重新尝试获取互斥锁(QMutex)并继续执行 // 参数说明: // 无参数 // 返回值: // 无(void)wakeAll
#include <QWaitCondition> void QWaitCondition::wakeAll(); // 功能: // 唤醒所有正在等待该 QWaitCondition 的线程 // 所有被唤醒的线程会依次尝试重新获取互斥锁(QMutex)并继续执行 // 参数说明: // 无参数 // 返回值: // 无(void)1.3 信号量
1.了解
有时在多线程编程中,需要确保多个线程能够并发访问数量有限的同一类资源。例如,设备内存有限,我们希望需要大量内存的线程能够根据可用内存数量来决定行为。这类问题通常用信号量来解决。信号量可以看作是增强版的互斥锁,不仅能完成加锁和解锁,还能跟踪可用资源的数量。
特点:QSemaphore是 Qt 提供的计数信号量类,用于控制同时访问共享资源的线程数量。
用途:限制并发线程数,解决资源有限的场景。
2.代码
//.h class SemaphoreApiDemo : public QObject { Q_OBJECT public: explicit SemaphoreApiDemo(QObject *parent = nullptr); }; #endif //.cpp SemaphoreApiDemo::SemaphoreApiDemo(QObject *parent) : QObject(parent) { // 1. 创建一个初始计数为 2 的信号量(同时允许两个线程访问资源) QSemaphore semaphore(2); qDebug() << "初始信号量可用计数:" << semaphore.available(); // 2. 第一次 acquire():计数减1,变成 1 semaphore.acquire(); qDebug() << "执行一次 acquire() 后,计数 = " << semaphore.available(); // 3. 第二次 acquire():计数减1,变成 0 semaphore.acquire(); qDebug() << "执行第二次 acquire() 后,计数 = " << semaphore.available(); // 4. 第三次 acquire():此时计数为0,调用会阻塞。 // 为了不卡死程序,我们用 tryAcquire() 展示非阻塞版本(返回 false 而不阻塞)。 bool success = semaphore.tryAcquire(); // 立即返回,不阻塞 qDebug() << "第三次尝试获取(tryAcquire)结果:" << success << ",计数仍为" << semaphore.available(); // 5. 执行 release():释放一个资源,计数从0变回1 semaphore.release(); qDebug() << "执行一次 release() 后,计数 = " << semaphore.available(); }3.方法
available
#include <QSemaphore> int available() const; // 功能: // 返回当前可用的资源数量 // 参数说明: // 无参数 // 返回值: // int: // 当前可用的资源数量,该值永远不会为负数semaphore
#include <QSemaphore> QSemaphore::QSemaphore( int n = 0 ); // 功能: // 构造一个信号量,并初始化可用的资源数量 // 参数说明: // int n = 0: // 信号量初始可用的资源数量(不可为负数,默认为 0) // 返回值: // 无(void)acquire
#include <QSemaphore> void QSemaphore::acquire( int n = 1 ); // 功能: // 尝试获取 n 个资源。如果没有足够的资源可用,调用将阻塞直到资源可用 // 参数说明: // int n = 1: // 需要获取的资源数量(默认为 1) // 返回值: // 无(void)release
#include <QSemaphore> void QSemaphore::release( int n = 1 ); // 功能: // 释放 n 个资源,使可用资源数量增加 // 参数说明: // int n = 1: // 需要释放的资源数量(默认为 1) // 返回值: // 无(void)tryAcquire
#include <QSemaphore> // bool QSemaphore::tryAcquire( // int n = 1 // ); // 功能: // 尝试获取 n 个资源,不会阻塞,而是立即返回 // 参数说明: // int n = 1: // 需要尝试获取的资源数量(默认为 1) // 返回值: // bool: // - 若成功获取了 n 个资源,返回 true // - 若当前可用资源不足,返回 false,且不会获取任何资源二、Qt网络
1.提要
在进⾏⽹络编程之前, 需要在项⽬中的 .pro ⽂件中添加 network 模块
2.1 UDP
1.QUdpSocket(socket文件)
2.QNetworkDategram(UDP数据报数据)
3.代码
服务器
//.h private slots: void ProcessRequest();//处理函数 private: QUdpSocket* socket;//创建对象 //.cpp //1.设置窗口标题 this->setWindowTitle("服务器"); //2.实例化socket socket = new QUdpSocket(this); //3.连接信号槽,处理收到的请求 connect(socket,&QUdpSocket::readyRead,this,&Widget::ProcessRequest); //4.绑定端口 bool ret = socket->bind(QHostAddress::Any,9090); if(!ret) { QMessageBox::critical(nullptr, "服务器启动出错", socket->errorString()); return; } void Widget::ProcessRequest() { //1.读取数据 const QNetworkDatagram& requestDatagram = socket->receiveDatagram(); QString request = requestDatagram.data(); //2.对数据进行处理 const QString& response= Process(request); //3.把响应结果写到客户端 QNetworkDatagram responseDatagram(response.toUtf8(),requestDatagram.senderAddress(), requestDatagram.senderPort()); socket->writeDatagram(responseDatagram); // 显⽰打印⽇志 QString log = "[" + requestDatagram.senderAddress().toString() + ":" + QString::number(requestDatagram.senderPort()) + "] req: " + request + ", resp: " + response; ui->listWidget->addItem(log); } QString Widget::Process(const QString request) { return request; }客户端
//.h QUdpSocket* socket; //.cpp //1.设置窗口名字 this->setWindowTitle("客户端"); //2.实例化socket socket = new QUdpSocket(this); //用于显示回响到客户端 // connect(socket,&QUdpSocket::readyRead,this,[=](){ // const QNetworkDatagram responDatagram = socket->receiveDatagram(); // QString response = responDatagram.data(); // ui->listWidget->addItem(QString("服务器说:")+response); // }); } void Widget::on_pushButton_clicked() { //1.获取输入框的内容 const QString& text = ui->lineEdit->text(); //2.构建请求数据 QNetworkDatagram requestDatagram(text.toUtf8(),QHostAddress(SERVER_IP),SERVER_PORT); //3.发送请求 socket->writeDatagram(requestDatagram); //4.将消息添加到列表框 ui->listWidget->addItem("客户端说:" + text); //5.清空输入框 ui->lineEdit->setText(""); }2.2 TCP
1.QTcpServer(用于监听端口,获取客户端连接)
2.QTcpSocket(用户端和服务器之间的数据交互)
3.代码
服务器
//.h public: QString Process(const QString&);//回响函数 private slots: void ProcessConnection();//处理流程 private: QTcpServer* tcpServer;//创建QTcpServer //.cpp //1.设置窗口标题 this->setWindowTitle("服务器"); //2.实例化TcpServer tcpServer = new QTcpServer(this); //3.通过信号槽,处理客户端建立的连接 connect(tcpServer,&QTcpServer::newConnection,this,&Widget::ProcessConnection); //4.监听端口 bool ret = tcpServer->listen(QHostAddress::Any,9090); if(!ret) { qDebug() << "服务器启动失败" << tcpServer->serverError(); exit(1); } void Widget::ProcessConnection() { //1.获取新连接的对应的socket QTcpSocket* clientSocket = tcpServer->nextPendingConnection(); QString log = QString("[") + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客⼾端上线!"; ui->listWidget->addItem(log); //2.通过信号槽,处理收到的请求情况 connect(clientSocket,&QTcpSocket::readyRead,this,[=]() { //读取 QString request = clientSocket->readAll(); //根据请求处理响应 const QString& response = Process(request); //把响应写回客户端 clientSocket->write(response.toUtf8()); QString log = QString("[") + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] req: " + request + ", resp: " + response; ui->listWidget->addItem(log); }); //3.通过信号槽,处理断开连接的情况 connect(clientSocket, &QTcpSocket::disconnected, this, [=]() { QString log = QString("[") + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客⼾端下线!"; ui->listWidget->addItem(log); // 删除 clientSocket clientSocket->deleteLater();//等待当前槽函数及所有关联事件处理完毕后,由事件循环自动销毁 }); } QString Widget::Process(const QString &request) { return request; }客户端
//.h private: QTcpSocket* tcpClient; //.cpp // 1. 设置窗⼝标题. this->setWindowTitle("客⼾端"); // 2. 实例化 socket 对象. tcpClient = new QTcpSocket(this); // 3. 和服务器建⽴连接. tcpClient->connectToHost(QHostAddress("127.0.0.1"), 9090); // 4. 等待并确认连接是否出错. if(!tcpClient->waitForConnected()) { qDebug()<<"连接服务器出场:" << tcpClient->errorString(); exit(1); } void Widget::on_pushButton_clicked() { //获取输入框内容 const QString& text = ui->lineEdit->text(); //把消息显示到界面上 ui->listWidget->addItem(QString("客户端说:") + text); //发送给服务器 tcpClient->write(text.toUtf8()); //情况输入框内容 ui->lineEdit->setText(""); }2.3 HTTP
1.方法
2.代码
//.h private slots: void on_pushButton_clicked(); private: QNetworkAccessManager* manager; //.cpp manager = new QNetworkAccessManager(this); void Widget::on_pushButton_clicked() { //1.获取输入框的URL,构造QUrl对象 QUrl url(ui->lineEdit->text()); //2.构造HTTP请求对象(1) QNetworkRequest request(url); //3.发送GET请求(2) QNetworkReply* response = manager->get(request); // 4. 通过信号槽来处理响应 connect(response, &QNetworkReply::finished, this, [=]() { if (response->error() == QNetworkReply::NoError) { // 响应正确 QString html(response->readAll()); ui->plainTextEdit->setPlainText(html); } else { // 响应出错 ui->plainTextEdit->setPlainText(response->errorString()); } response->deleteLater(); }); }