QTcpServer运行在子线程
创建一个QThread和QWorker(继承自QObject)类对象,使用moveToThread函数移动到thread中运行,通过thread类start信号和worker的init槽函数绑定,init槽函数内是一些初始化操作,然后定义个定时器,周期触发doWork()。
网上有很多教程是在doWork()中使用while(isRunning)死循环的形式,不建议这么干,如果线程一直在doWork中死循环,那么他是无法接收到来自外部的信号的。推荐的方法是用定时器周期触发。
workserver.h
#ifndef WORKSERVER_H
#define WORKSERVER_H
#include <QObject>
#include <QThread>
#include <QTcpServer>
#include <QTcpSocket>
#include <QByteArray>
#include <QDebug>
class WorkServer : public QObject
{
Q_OBJECT
public:
explicit WorkServer(QObject *parent = nullptr);
~WorkServer();
signals:
void sigRecv(QByteArray ba, quint32 cnt);
private:
QTcpServer* m_server;
};
#endif // WORKSERVER_H
workserver.cpp
#include "workserver.h"
WorkServer::WorkServer(QObject *parent) : QObject(parent)
{
static quint32 count = 0;
qDebug()<<"WorkServer thread ID:"<<QThread::currentThreadId();
m_server = new QTcpServer(this);
m_server->listen(QHostAddress::Any, 8888);
connect(m_server, &QTcpServer::newConnection, this, [=](){
QTcpSocket* tcpsock = m_server->nextPendingConnection();
connect(tcpsock, &QTcpSocket::readyRead, this, [=](){
QByteArray ba = tcpsock->readAll();
count++;
emit sigRecv(ba, count);
tcpsock->write("5566");
// qDebug()<<"tcpsock recv:"<<ba;
// qDebug()<<"WorkServer thread ID:"<<QThread::currentThreadId();
});
connect(tcpsock, &QTcpSocket::disconnected, this, [=](){
qDebug()<<"tcpsock disconnected...";
tcpsock->deleteLater();
qDebug()<<"WorkServer thread ID:"<<QThread::currentThreadId();
count = 0;
});
});
}
WorkServer::~WorkServer()
{
// m_server->deleteLater();
}
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QThread>
#include "workserver.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
public slots:
void clientSend(QByteArray ba, quint32 cnt);
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
QThread* m_threadServer;
WorkServer* m_workServer;
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
qDebug()<<"main thread ID:"<<QThread::currentThreadId();
m_threadServer = new QThread; //不要指定parent
m_workServer = new WorkServer; //不要指定parent
connect(m_workServer, &WorkServer::sigRecv, this, &Widget::clientSend);
connect(m_threadServer, &QThread::finished, this, [=](){
m_threadServer->quit();
m_threadServer->deleteLater();
});
m_workServer->moveToThread(m_threadServer);
m_threadServer->start();
}
Widget::~Widget()
{
// m_threadServer->quit();
m_workServer->deleteLater();
delete ui;
}
void Widget::clientSend(QByteArray ba, quint32 cnt)
{
QString str(ba);
QString str2 = QString::number(cnt);
str += " cnt: " + str2;
ui->te_Recv->append(str);
}
void Widget::on_pushButton_clicked()
{
ui->te_Recv->clear();
}
运行结果
两种方法的比较
两种方法来执行线程都可以,随便你的喜欢。不过看起来第二种更加简单,容易让人理解。不过我们的兴趣在于这两种使用方法到底有什么区别?其最大的区别在于:
-
moveToThread方法,是把我们需要的工作全部封装在一个类中,将每个任务定义为一个的槽函数,再建立触发这些槽的信号,然后把信号和槽连接起来,最后将这个类调用moveToThread方法交给一个QThread对象,再调用QThread的start()函数使其全权处理事件循环。于是,任何时候我们需要让线程执行某个任务,只需要发出对应的信号就可以。其优点是我们可以在一个worker类中定义很多个需要做的工作,然后发出触发的信号线程就可以执行。相比于子类化的QThread只能执行run()函数中的任务,moveToThread的方法中一个线程可以做很多不同的工作(只要发出任务的对应的信号即可)。
-
子类化QThread的方法,就是重写了QThread中的run()函数,在run()函数中定义了需要的工作。这样的结果是,我们自定义的子线程调用start()函数后,便开始执行run()函数。如果在自定义的线程类中定义相关槽函数,那么这些槽函数不会由子类化的QThread自身事件循环所执行,而是由该子线程的拥有者所在线程(一般都是主线程)来执行。如果你不明白的话,请看,第二个例子中,子类化的线程的槽函数中输出当前线程的ID,而这个ID居然是主线程的ID!!事实的确是如此,子类化的QThread只能执行run()函数中的任务直到run()函数退出,而它的槽函数根本不会被自己的线程执行。
同步QThread的类
为了同步线程,Qt提供了QMutex、QReadWriteLock、QSemaphore和QWaitCondition类。主线程等待与其他线程的中断时,必须进行同步。例如:两个线程同时访问共享变量,那么可能得不到预想的结果。因此,两个线程访问共享变量时,必须进行同步。
- 一个线程访问指定的共享变量时,为了禁止其他线程访问,QMutex提供了类似锁定装置的功能。互斥体激活状态下,线程不能同时访问共享变量,必须在先访问的线程完成访问后,其他线程才可以继续访问。
- 一个线程访问互斥体锁定的共享变量期间,如果其他线程也访问此共享变量,那么该线程将会一直处于休眠状态,直到正在访问的线程结束访问。这称为线程安全。
- QReadWriteLock和QMutex的功能相同,区别在于,QReadWriteLock对数据的访问分为读访问和写访问。很多线程频繁访问共享变量时,与QMetex相对,使用QReadWriteLock更合适。
- QSemaphore拥有和QMutex一样的同步功能,可以管理多个按数字识别的资源。QMutex只能管理一个资源,但如果使用QSemaphore,则可以管理多个按号码识别的资源。
- 条件符合时,QWaitCondition允许唤醒线程。例如,多个线程中某个线程被阻塞时,通过QWaitCondition提供的函数wakeOne()和wakeAll()可以唤醒该线程。
可重入性与线程安全
- 可重入性:两个以上线程并行访问时,即使不按照调用顺序重叠运行代码,也必须保证结果;
- 线程安全:线程并行运行的情况下,虽然保证可以使程序正常运行,但访问静态空间或共享(堆等内存对象)对象时,要使用互斥体等机制保证结果。
一个线程安全的函数不一定是可重入的;一个可重入的函数缺也不一定是线程安全的!
可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个线程调用此函数时,很有可能使有关全局变量变为不可知状态。
满足下列条件的函数多数是不可重入的:
- 函数体内使用了静态的数据结构和全局变量,若必须访问全局变量,利用互斥信号量来保护全局变量;
- 函数体内调用了malloc()或者free()函数;
- 函数体内调用了标准I/O函数。
常见的不可重入函数有:
- printf --------引用全局变量stdout
- malloc --------全局内存分配表
- free --------全局内存分配表
也就是说:本质上,可重入性与C++类或者没有全局静态变量的函数相似,由于只能访问自身所有的数据变量区域,所以即使有两个以上线程访问,也可以保证安全性。