Qt 线程

Qt中线程的解释: 

QThread(线程),代表一个在应用程序中可以独立控制的线程,可以和进程中的其他线程分享数据。QThread 对象管理程序中的一个控制线程。QThreads 在 run() 中开始执行。默认情况下,run() 通过调用 exec() 来启动事件循环,并在线程内运行 Qt 事件循环

线程的使用条件:常用于比较耗时的任务,使用线程执行的话,主界面才能正常运行。 

创建线程的方法:

  1. 继承QThread  重写run函数
  2. 继承QObject  通过moveToThread(thread),交给thread执行
  3. 继承QRunnable 重写run函数,但必须使用线程池(QThreadPool)运行

继承QThread的特点:

  1. 可以通过信号槽与外界进行通信
  2. 线程中的对象必须在run函数中创建
  3. 每次新建一个线程都需要继承QThread,实现一个新类,使用不太方便。 
  4. 要自己进行资源管理,线程释放和删除。并且频繁的创建和释放会带来比较大的内存开销。

(适用场景:那些常驻内存的任务)


继承QObject  通过moveToThread(thread)的特点:

  1. 使用比较简单方便
  2. 但只能执行槽函数中的内容,需要通过信号与槽的方式连接
  3. 创建对象时不能指定父类,如果指定父类,那么将无法调用到线程中

 注意:线程中无法使用任何界面部件类

 第一种创建方式:

class Thread : public QThread //继承QThread
{
    Q_OBJECT
public:
    explicit Thread(QObject *parent = nullptr);
protected:
    void run() override;//重写run函数
};

void Thread::run()
{
  //执行比较耗时的任务
}


//在主类中调用

Thread  thread1;

thread.start();//开起线程,就会执行run函数

第二种创建方式:

class Worker : public QObject //继承自QObject
{
    Q_OBJECT

public slots:
    void doWork(const QString ¶meter) {
        QString result;
        /* ... here is the expensive or blocking operation ... */
        emit resultReady(result);
    }

signals:
    void resultReady(const QString &result);
};

class Controller : public QObject
{
    Q_OBJECT
    QThread workerThread;//创建一个线程
public:
    Controller() {
        Worker *worker = new Worker;//创建一个工作对象
        worker->moveToThread(&workerThread);//移动到线程种执行
        //当线程执行完成后,销毁work对象
        connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
        connect(this, &Controller::operate, worker, &Worker::doWork);
        connect(worker, &Worker::resultReady, this, &Controller::handleResults);
        workerThread.start();//开启线程
    }
    ~Controller() {
        workerThread.quit();//关闭线程
        workerThread.wait();//堵塞线程
    }
public slots:
    void handleResults(const QString &);
signals:
    void operate(const QString &);

QThread 常用的函数:

exec()

进入事件循环并等待 exit() 被调用,返回传递给exit() 的值。如果通过 quit() 调用 exit(),则返回的值为 0。

此函数旨在从 run() 中调用。必须调用此函数才能启动事件处理

exit() 告知线程的事件循环以返回代码退出
currentThread() 返回指向管理当前正在执行的线程的 QThread 的指针。
idealThreadCount() 返回可在系统上运行的理想线程数,如果无法检测到处理器内核数,则此函数返回 1。
isDone() 如果线程完成返回true 否则返回 false
isRunning() 如果线程正在运行返回true 否则返回 false
msleep() 强制当前线程休眠毫秒
sleep() 强制当前线程休眠几秒钟,此功能不保证准确性。在重负载条件下,应用程序可能会休眠超过
usleep() 强制当前线程休眠 usecs微秒,此功能不保证准确性。在重负载条件下,应用程序可能比 usecs 休眠时间更长
requestInterruption() 请求中断线程。该请求是建议性的,由线程上运行的代码决定是否以及如何对此类请求进行操作。此函数不会停止线程上运行的任何事件循环,也不会以任何方式终止它。
run() 线程的起点,调用 start() 后,新创建的线程调用此函数,您可以重新实现此函数以促进高级线程管理
quit() 告知线程的事件循环退出并返回代码 0(成功)。等效于调用 QThread::exit(0)
wait() 阻塞线程,直到如果线程已完成,此函数将返回 true。如果线程尚未启动,它也返回 true。或到了最后期限。如果达到截止时间,此函数将返回 false
start() 通过调用 run() 开始执行线程。操作系统将根据优先级参数调度线程。如果线程已在运行,则此函数不执行任何操作
terminate() 终止线程的执行。线程可能会也可能不会立即终止,具体取决于操作系统的调度策略。在 terminate() 之后使用 QThread::wait() 可以肯定。

signals

finished() 此信号在完成执行之前从关联的线程发出
started() 在调用run() 函数之前,当关联线程开始执行时,此信号从关联线程发出

例子:

主界面有,开启,暂停,关闭按钮,在一个QLabel显示线程中的数据

创建一个myThread类继承自QThread

class myThread : public QThread
{
    Q_OBJECT
public:
    explicit myThread(QObject *parent = nullptr);

    void stop_thread()//暂停
    {
        thread_run=false;//设置为不可执行
    }

    void run()//重写run函数    (函数直接在这里实现,比较好查看)
    {
        while(thread_run)//如果可以执行
        {
            a+=10;
            emit show_number(a);//触发信号
            sleep(1);//休眠5秒
        }
        thread_run=true;

    }
protected:
    volatile bool thread_run=true;//判断是否可以运行
    int a=0;

signals:
    void show_number(int i);//发送信号给主窗口,显示数据

};

主类中的使用:

Qt 线程_第1张图片

.cpp文件中:

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    myThread *thread=new myThread;
    qDebug()<<"本机线程数"<idealThreadCount();//查看自己的线程数
    connect(ui->pushButton,&QPushButton::clicked,[=]()//开启线程
    {
        thread->start();
        qDebug()<<"线程已开启";
        qDebug()<<"线程id"<currentThreadId();
    });
    connect(ui->pushButton_2,&QPushButton::clicked,[=]()//暂停线程
    {
        thread->stop_thread();//暂停
        qDebug()<<"线程已暂停";
    });
    connect(ui->pushButton_3,&QPushButton::clicked,[=]()//结束线程
    {
        ui->lcdNumber->display(0);
        thread->stop_thread();//暂停
        thread->quit();
        thread->wait();
        thread->deleteLater();//销毁
        ui->pushButton->setEnabled(false);//开启不可点击
    });
    connect(thread,SIGNAL(show_number(int)),this,SLOT(show_data(int)));

}
void Widget::show_data(int s)
{
    ui->lcdNumber->display(s);
}

Widget::~Widget()
{
    delete ui;
}

重入和线程安全:

在整个文档中,重入和线程安全用于标记类和函数,从而表明怎样在多线程应用中使用它们。

  • 线程安全函数可以从多个线程同时调用,即使调用使用共享数据也是如此,因为对共享数据的所有引用都是序列化的。
  • 也可以从多个线程同时调用重函数,但前提是每个调用都使用自己的数据

以上可以得出结论:线程安全函数始终是可重入的,但可重入函数并不总是线程安全的。

通过扩展,如果可以从多个线程安全地调用一个类的成员函数,只要每个线程使用该类的不同实例,则该类就被称为可重入类。如果可以从多个线程安全地调用该类的成员函数,则该类是线程安全的,即使所有线程都使用该类的同一实例也是如此。

重入:

c++类通常是可重入的,因为它们只能访问自己的成员数据。任何线程都可以在可重入类的实例上调用成员函数,只要没有其他线程可以同时调用该类的同一实例上的成员函数。

例如:

class Counter
{
public:
    Counter() { n = 0; }

    void increment() { ++n; }
    void decrement() { --n; }
    int value() const { return n; }

private:
    int n;
};

上面的类是可重入的,但这并不是线程安全的,当有多个线程同时修改类的一个成员变量,可能会

会产生多种结果 。因为++和--的操作并不是总是原子的(原子操作是指一个操作不会被其他线程中断)。它们会分为3个机械指令:

  • 在寄存器中加载变量的值
  • 递增或递减寄存器的值
  • 将寄存器的值存储回主存储器

当有多个线程同时加载变量的旧值,然后递增它们的寄存器,然后将其存储回去,最终它们会相互覆盖,变量只会递增一次。

线程安全:

显然,当有多个线程访问时,访问必须序列化,当有一个线程访问时,其他线程必须等待。

例如:使用QMutex保护数据的访问。

class Counter
{
public:
    Counter() { n = 0; }

    void increment() { QMutexLocker locker(&mutex); ++n; }
    void decrement() { QMutexLocker locker(&mutex); --n; }
    int value() const { QMutexLocker locker(&mutex); return n; }

private:
    mutable QMutex mutex;
    int n;
};

在构造函数中自动锁定互斥锁,在函数结束时调用析构函数时解锁互斥锁,锁定互斥锁可确保序列化来自不同线程的访问。数据成员mutex是用限定符mutable声明的,因为我们需要锁定和解锁value中的互斥锁,因为这是一个 const 函数。

 同步线程:

线程的目的是允许并行运行,但有时线程必须停止等待其他线程。例如,如果两个线程尝试访问同一个变量,这样的话结果是未定义的。强制线程相互等待的原则成为互斥,是一种保护共享资源的常用技术。

同步线程类:

  • QMutex  互斥锁
  • QReadWriteLock  读-写锁
  • QSemaphore  信号量
  • QWaitCondition 条件变量

QMutex(互斥锁)

提供一个互斥锁,在任何事件至多有一个线程可以获得mutex。如果一个线程尝试获得mutex,而mutex已经锁住,那么这个线程将会睡眠

QReadWriteLock  (读-写锁)

读-写锁,于QMutex相似,但它对共享数据进行访问的区分,分为“读”,“写”访问,允许多个线程同时对数据进行“读”访问,QReadWiteLock的并行度大于QMutex

QSemaphore  信号量

QSemaphore是QMutex的概括,可保护一定数量的相同资源。相比之下,QMutex 只保护一种资源。信号量示例显示了信号量的典型应用:同步对生产者和使用者之间循环缓冲区的访问

QWaitCondition 条件变量

QWaitCondition,允许一个线程在一些条件满足时唤醒其他线程, 不是通过强制执行互斥而是通过提供条件变量来同步线程。一个或多个线程可以被堵塞来等待一个QWaitCondition,使用wakeOne()可以唤醒一个随机选取的等待的线程,使用wakeAll()可以唤醒所有正在等待的线程

 QMutex

QMutex通常和QMutexLocker一起使用,才可以确保这可以轻松确保锁定和解锁的执行一致。

常用函数:

isRecursive() 判断互斥锁是否为递归(Qt 5.7引入)
lock() 锁定互斥锁,如果另一个线程锁定了互斥锁,则此调用将阻塞,直到该线程解锁它
tryLock(int ) 尝试锁定互斥锁,如果已获得锁,则函数返回true 否则返回 false,可以设置等待时间。如果获得锁则必须使用unlock()解锁互斥锁,这样才能在另一个线程才能成功锁定它。
try_lock() 尝试锁定互斥锁,如果已获得锁,则函数返回true 否则返回 false,提供此功能是为了与标准库概念兼容,等价于tryLock()(Qt  5.8 引入)
try_lock_for()

尝试锁定互斥锁,如果已获得锁,则函数返回true 否则返回 false,

如果获得锁,必须使用unlock()解锁互斥锁

如果为递归互斥锁,允许在同一线程的容易个互斥锁上多次调用该函数

如果此互斥锁是非递归互斥锁,则在尝试递归锁定互斥锁时,此函数将始终返回 false。(Qt  5.8 引入)

try_lock_until()

尝试锁定互斥锁,如果已获得锁,则函数返回true 否则返回 false,

如果获得锁,必须使用unlock()解锁互斥锁

如果为递归互斥锁,允许在同一线程的容易个互斥锁上多次调用该函数

如果此互斥锁是非递归互斥锁,则在尝试递归锁定互斥锁时,此函数将始终返回 false。(Qt  5.8 引入)

unlock() 解锁互斥锁。

 QMutex::QMutex(QMutex::RecursionMode mode)

创建一个互斥锁时可以设置模式

QMutex::Recursive 在这种模式下,线程可以多次锁定同一个互斥锁,并且在进行相应数量的 unlock() 调用之前,互斥锁不会被解锁
QMutex::NonRecursive 在此模式下,线程只能锁定一次互斥锁

互斥锁使用场景:当一个变量同时被多个线程访问

int number=10;

void text()
{
    number*=2;
    number+=5;
}
void text1()
{
    number+=10;
    number*3;
}

正常调用 text()和text1()的话

text()  number=2*10=20+5=25
text1() number=25+10=30*3=90

同时调用的话可能会发生以下情况

线程1调用text()
number=10*2=20;
线程2调用text1(),现在text()的调用暂停
number=20+10=30
number=30*3=90;
继续完成线程text()
number=90+5=95

为了防止以上情况,可以上个互斥锁,使得调用完某个函数才能调用其他函数。

QMutex mutex;//互斥锁

int number=10;

void text()
{
     mutex.lock();//上锁
     number*=2;
     number+=5;
     mutex.unlock();//解锁  
}

void text1()
{
     mutex.lock();//上锁
     number+=10;
     number*=3;
     mutex.unlock();//解锁  
}

QMutexLocker类

QMutexLocker类是一个方便点的类,可以简化锁定和解锁的互斥锁。

使用方法:QMutexLocker应该在需要锁定QMutex的函数中创建,创建一个QMutexLock时,互斥锁被锁定。(更加方便)

函数:

mutex(() 返回正在运行的互斥锁
relock() 重新锁定未锁定的互斥锁锁。
unlock() 解锁此互斥锁

 使用QMutex的情况:需要在分支中解锁互斥锁

int complexFunction(int flag)
{
    mutex.lock();

    int retVal = 0;

    switch (flag) {
    case 0:
    case 1:
        retVal = moreComplexFunction(flag);
        break;
    case 2:
        {
            int status = anotherFunction();
            if (status < 0) {
                mutex.unlock();
                return -2;
            }
            retVal = status + flag;
        }
        break;
    default:
        if (flag > 10) {
            mutex.unlock();
            return -1;
        }
        break;
    }

    mutex.unlock();
    return retVal;
}

QMutexLocker的使用

int complexFunction(int flag)
{
    QMutexLocker locker(&mutex);//创建一个QmutexLocker对象

    int retVal = 0;

    switch (flag) {
    case 0:
    case 1:
        return moreComplexFunction(flag);
    case 2:
        {
            int status = anotherFunction();
            if (status < 0)
                return -2;
            retVal = status + flag;
        }
        break;
    default:
        if (flag > 10)
            return -1;
        break;
    }

    return retVal;
}

当函数执行完后,QMutexLocker对象销毁时,互斥锁会解锁,就不用每个分支去解锁。

使用QMutexLocker::mutex()可以获取当前正在运行的互斥锁。 

 QReadWriteLock(读-写锁)

读写锁是一种同步工具,用于保护可以访问以进行读取和写入的资源。如果要允许多个线程同时具有只读访问权限,则这种类型的锁定很有用,但是一旦一个线程想要写入资源,就必须阻止所有其他线程,直到写入完成。

函数:

lockForRead() 锁定读取,

如果另一个线程已锁定写入,此函数将阻止当前线程。

如果线程已锁定写入,则无法锁定读取。

lockForWrite()

锁定写入

如果另一个线程(包括当前线程)已锁定以进行读取或写入,则此函数将阻止当前线程

tryLockForRead() 尝试锁定以进行读取,如果线程已锁定写入,则无法锁定读取
tryLockForWrite() 尝试锁定以进行写入,如果线程已锁定以进行读取,则无法锁定写入
unlock() 解锁

QReadWriteLock通常使用于经常读取数据,可以多个线程同时读取数据。

QReadWriteLock lock;

void ReaderThread::run()
{
    ...
    lock.lockForRead();
    read_file();
    lock.unlock();
    ...
}

void WriterThread::run()
{
    ...
    lock.lockForWrite();
    write_file();
    lock.unlock();
    ...
}

 QSemaphore(信号量)

 信号量是互斥锁的概括。虽然互斥锁只能锁定一次,但可以多次获取信号量。信号量通常用于保护一定数量的相同资源

函数:

acquire(int n) 尝试获取n个由信号量保护的资源,当资源不够时将堵塞直到资源足够。
release(int n)

释放由信号量保护的 n 个资源,

此函数也可用于“创建”资源。

available() 返回信号灯当前可用的资源数。这个数字永远不能是负数。
tryAcquire(int n) 尝试获取由信号量保护的资源,成功返回true,否则返回false

tryAcquire(int n,int timeout

尝试获取由信号量保护的资源,成功返回true,否则返回false,调用最多等待timeout秒

 QSemaphore的创建

QSemaphore::QSemaphore(int n=0)

 创建新的信号量,并将其保护的资源数初始化为 n(默认为 0)。

    QSemaphore sem(5);
    sem.acquire(3);
    qDebug()<<"资源还有"<

当释放的资源多余需要释放的资源时,多余的会进行创建

    QSemaphore sem(5);
    sem.acquire(3);
    qDebug()<<"资源还有"<

 

当资源少于需要获取的资源时,不会获取成功。

    QSemaphore sem(5);
    if(sem.tryAcquire(7)){
        qDebug()<<"获取成功";
    }
    else
    {
        qDebug()<<"获取失败";
    }

 QSemaphore信号量的生产者消费者问题。

全局变量 :

const int DataSize=1000;//生产者的数据量
const int BufferSize=800;//缓冲区大小
char buffer[Buffersize];
QSemaphore freeBytes(BufferSize);//控制缓冲区的信号量
QSemaphore usedBytes;//控制已经使用的缓冲区

 生产者类:

class Producer :public QThread
{
public:
   void run();
}

void Producer::run()
{
    qsrand(QTime(0.0.0).secsTo(QTime::currentTime()));//随机数
    for(int i=0;i

消费者类:

class Consumer :public QThread
{
public:
   void run();
}

void Producer::run()
{
    for(int i=0;i

 main()

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    Producer producer;//生产者
    Consumer consumer;//消费者
    producer.start();//开启线程
    consumer.start();//开启线程
    //两个线程调用wait(),阻塞线程,确保两个线程在退出前都有时间能完成main()
    producer.wait();
    consumer.wait();
    return 0;
}

 QWaitCondition

允许一个线程在一些条件满足时唤醒其他线程, 不是通过强制执行互斥而是通过提供条件变量来同步线程。一个或多个线程可以被堵塞来等待一个QWaitCondition,使用wakeOne()可以唤醒一个随机选取的等待的线程,使用wakeAll()可以唤醒所有正在等待的线程

函数:

wait(QMutex*,time) 释放锁定的互斥锁并等待等待条件
wakeOne() 唤醒一个等待等待条件的线程。唤醒的线程取决于操作系统的调度策略,无法控制或预测。如果要唤醒特定线程,解决方案通常是使用不同的等待条件,并让不同的线程等待不同的条件
wakeAll() 唤醒等待等待条件的所有线程。线程的唤醒顺序取决于操作系统的调度策略,无法控制或预测。
notify_one()

提供此函数是为了与 STL 兼容。它等效于 wakeOne()Qt 5.8中引入

notify_all() 提供此函数是为了与 STL 兼容。它等效于 wakeAll()Qt 5.8中引入

 QWaitCondition的示例:

使用QWaitCondition的QMutex解决生产者-消费者问题

设置全局变量: 

const int DataSize = 100000;//生产者将生成的数据量

const int BufferSize = 8192;//缓冲区
char buffer[BufferSize];

//两个等待条件,一个互斥锁和计数器
QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;

生产者类:

class Producer : public QThread
{
public:
    Producer(QObject *parent = NULL) : QThread(parent)
    {
    }

    void run() override
    {
        for (int i = 0; i < DataSize; ++i) {
            mutex.lock();//上锁
            if (numUsedBytes == BufferSize)//检查缓冲区是否已满
                bufferNotFull.wait(&mutex);//已满的话等待条件满足
            mutex.unlock();//解锁
            //存放数据(随机数)
            buffer[i % BufferSize] = "ACGT"[QRandomGenerator::global()->bounded(4)];
            
            mutex.lock();//上锁
            ++numUsedBytes;计数器+1
            bufferNotEmpty.wakeAll();//唤醒全部线程
            mutex.unlock();//解锁
        }
    }
};

消费者类:

class Consumer : public QThread
{
    Q_OBJECT
public:
    Consumer(QObject *parent = NULL) : QThread(parent)
    {
    }

    void run() override
    {
        for (int i = 0; i < DataSize; ++i) {
            mutex.lock();
            if (numUsedBytes == 0)//检查缓冲区是否为空
                bufferNotEmpty.wait(&mutex);
            mutex.unlock();

            fprintf(stderr, "%c", buffer[i % BufferSize]);//输出内容

            mutex.lock();
            --numUsedBytes;//计数器-1
            bufferNotFull.wakeAll();
            mutex.unlock();
        }
        fprintf(stderr, "\n");
    }

signals:
    void stringConsumed(const QString &text);
};

main函数:

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    Producer producer;//生产者
    Consumer consumer;//消费者
    producer.start();//开启线程
    consumer.start();//开启线程
    //两个线程调用wait(),阻塞线程,确保两个线程在退出前都有时间能完成main()
    producer.wait();
    consumer.wait();
    return 0;
}

你可能感兴趣的:(Qt网络和线程,qt)