QT5多线程

QT5多线程

通常情况下,应用程序都是在一个线程中执行操作。但是,当调用一个耗时操 作(例如,大批量I/O或大量矩阵变换等CPU密集操作)时,用户界面常常会冻结。而使用多线程可解决这一问题。

多线程具有以下几点优势。

(1) 提高应用程序的响应速度。这对于开发图形界面的程序尤为重要,当一个操作耗时很长时,整个系统都会等待这个操作,程序就不能响应键盘、鼠标、菜单等的操作,而使用多线程技术可将耗时长的操作置于一个新的线程,从而避免以上 的问题。

(2) 使多CPU系统更加有效。当线程数不大于CPU数目时,操作系统可以调度不同的线程运行于不同的CPU上。

(3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为独立或半独立的运行部分,这样有利于代码的理解和维护。

多线程程序有以下几个特点。

(1) 多线程程序的行为无法预期,当多次执行上述程序时,每一次的运行结果都可能不同。

(2) 多线程的执行顺序无法保证,它与操作系统的调度策略和线程优先级等因素有关。

(3) 多线程的切换可能发生在任何时刻、任何地点。

(4) 多线程对代码的敏感度高,因此对代码的细微修改都可能产生意想不到的结果。

简单例子

示例代码:

ThreadDlg.cpp

#include "ThreadDlg.h"
#include 
ThreadDlg::ThreadDlg(QWidget *parent)
        :QDialog(parent)
{
    setWindowTitle (tr ( "线程"));
    startBtn = new QPushButton(tr("开始"));
    stopBtn = new QPushButton (tr("停止"));
    quitBtn = new QPushButton (tr ( "退出"));
    QHBoxLayout *mainLayout = new QHBoxLayout(this);
    mainLayout->addWidget(startBtn);
    mainLayout->addWidget(stopBtn);
    mainLayout->addWidget(quitBtn);
    connect(startBtn,SIGNAL(clicked()),this,SLOT(slotstart ()));
    connect(stopBtn,SIGNAL(clicked()),this, SLOT(slotstop()));
    connect(quitBtn, SIGNAL(clicked()),this,SLOT(close ()));
}

void ThreadDlg::slotstart()
{
    for (int i=0; i<MAXSIZE; i++)
    {
        workThread[i]= new WorkThread();
    }

    for (int i=0;i<MAXSIZE;i++)
    {
        workThread[i]->start();
    }
    startBtn->setEnabled(false);
    stopBtn->setEnabled(true);
}

void ThreadDlg::slotstop()
{
    for (int i=0; i<MAXSIZE; i++)
    {
        workThread[i]->terminate();
        workThread[i]->wait();
    }
    startBtn->setEnabled(true);
    stopBtn->setEnabled(false);
}

ThreadDlg.h

#ifndef TEST_THREADDLG_H
#define TEST_THREADDLG_H

#include 
#include 
#include "workthread.h"
#define MAXSIZE 1
class ThreadDlg : public QDialog
{
    Q_OBJECT
public:
    ThreadDlg(QWidget *parent = 0);
    ~ThreadDlg() override = default;

protected slots:
    void slotstart();
    void slotstop();

private:
    QPushButton *startBtn;
    QPushButton *stopBtn;
    QPushButton *quitBtn;
    WorkThread *workThread[MAXSIZE];
};

#endif //TEST_THREADDLG_H

WorkThread.cpp

#include "WorkThread.h"
#include 

WorkThread::WorkThread()
{

}

void WorkThread::run()
{
    while (true)
    {
        for (int n=0;n<10;n++)
            std::cout <

WorkThread.h

#ifndef TEST_WORKTHREAD_H
#define TEST_WORKTHREAD_H

#include 
class WorkThread : public QThread
{
Q_OBJECT
public:
    WorkThread();

protected:
    void run ();
};

#endif //TEST_WORKTHREAD_H

main.cpp

#include 
#include 

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    ThreadDlg tt;
    tt.show();
    return a.exec ();
}

示例图片:

QT5多线程_第1张图片

多线程控制

程之间存在着互相制约的关系, 具体可分为互斥和同步这两种关系。

实现线程的互斥与同步常使用的类有 QMutex、QMutexLocker、 QReadWriteLocker、 QReadLocker、QWriteLocker、QSemaphore 和 QWaitCondition。

class Key
(
public:
    Key() {key=0;}
    int creatKey() 
    {
        ++key; 
        return key;
    }
    int value()const 
    {
        return key;
    }
private:
    int key;
}

这是实现生成从0开始递增并且不允许重复的值的Key类。

在多线程环境下,这个类是不安全的,因为存在多个线程同时修改私有成员 key,其结果是不可预知的。

虽然类Key产生主键的函数creatKey()只有一条语句执行修改成员变量key的 值,但是C++的"++”操作符并不是原子操作,通常编译后它将被展开成为以下三 条机器命令:

•将变量值载入寄存器;

•将寄存器中的值加1:

•将寄存器中的值写回主存。

假设当前的key值为0,如果线程1和线程2同时将0值载入寄存器,执行加 1操作并将加1后的值写回主存,则结果是两个线程的执行结果将互相覆盖,实际 上仅进行了一次加1操作,此时的key值为1。

为了保证类Key在多线程环境下正确执行,上面的三条机器指令必须串行执行 且不允许中途被打断(原子操作),即线程1在线程2 (或线程2在线程1)之前完 整执行上述三条机器指令。

实际上,私有变量key是一个临界资源(Critical Resource, CR)。临界资源一 次仅允许一个线程使用,它可以是一块内存、一个数据结构、一个文件或者任何其 他具有排他性使用的东西。在程序中通常竞争使用临界资源。这些必须互斥执行的 代码段称为“临界区(Critical Section, CS)”,临界区(代码段)实施对临界资源 的操作,为了阻止问题的产生,一次只能有一个线程进入临界区。通常有相关的机 制或方法在程序中加上“进入”或“离开”临界区等操作,如果一个线程已经进入 某个临界区,则另一个线程就绝不允许在此刻再进入同一个临界区。

互斥量

互斥量可通过QMutex或者QMutexLocker类实现。

  1. QMutex 类

QMutex类是对互斥量的处理。它被用来保护一段临界区代码,即每次只允许 一个线程访问这段代码。

QMutex类的lock。函数用于锁住互斥量,如果互斥量处于解锁状态,则当前线 程就会立即抓住并锁定它,否则当前线程就会被阻塞,直到持有这个互斥量的线程 对它解锁。线程调用lock()函数后就会持有这个互斥量直到调用unlock()操作为止。

QMutex类还提供了一个tryLock()函数,如果互斥量已被锁定,则立即返回。

class Key
(
public:
    Key() {key=0;}
    int creatKey()	
    {
        mutex.lock() ; 
        ++key; 
        return key;
        mutex.unlock();
    }
    int value()const 
    {
        mutex.lock(); 
        return key; 
        mutex.unlock(); 
    } 
private:
    int key;
    QMutex mutex;
}

上述的代码段中,虽然creatKey()函数中使用mutex进行了互斥操作,但是 unlock()操作却不得不在return之后,从而导致unlock()操作永远无法执行。同样, value。函数也存在这个问题。

  1. QMutexLocker 类

Qt提供的QMutexLocker类可以简化互斥量的处理,它在构造函数中接收一个 QMutex对象作为参数并将其锁定,在析构函数中解锁这个互斥量,这样就解决了 以上问题。

class Key
{
public:
	Key() {key=0;}
    int creatKey() 
    {
        QMutexLocker.locker(&mutex); 
        ++key;
        return key; 
    }
    int value()const 
    { 
        QMutexLocker.locker(&mutex);
        return key; 
    } 
private:
    int key;
    QMutex mutex;
}

locker()函数作为局部变量会在函数退出时结束其作用域,从而自动对互斥量 mutex解锁。

在实际应用中,一些互斥量锁定和解锁逻辑通常比较复杂,并且容易出错,而使用 QMutexLocker类后,通常只需要这一条语句,从而大大简化了编程的复杂程度。

信号量

信号量可以理解为对互斥量功能的扩展,互斥量只能锁定一次而信号量可以获 取多次,它可以用来保护一定数量的同种资源。信号量的典型用例是控制生产者/ 消费者之间共享的环形缓冲区。

生产者/消费者实例中对同步的需求有两处:

(1) 如果生产者过快地生产数据,将会覆盖消费者还没有读取的数据。

(2) 如果消费者过快地读取数据,将越过生产者并且读取到一些过期数据。

针对以上问题,可以有两种解决方法:

(1) 首先使生产者填满整个缓冲区,然后等待消费者读取整个缓冲区,这是- 个比较笨拙的方法。

(2) 使生产者和消费者线程同时分别操作缓冲区的不同部分,这是一种比较高效 的方法。

示例代码:

main.cpp

#include 
#include 
#include 

const int DataSize=1000;
const int BufferSize=80;
int buffer[BufferSize];

QSemaphore freeBytes(BufferSize);
QSemaphore usedBytes(0);

class Producer : public QThread
{
public:
    Producer()
    {

    }
    void run()
    {
        for (int i = 0; i < DataSize; i++)
        {
            freeBytes.acquire();
            buffer[i % BufferSize] = (i % BufferSize);
            usedBytes.release();
        }
    }

};

class Consumer : public QThread
{
public:
    Consumer()
    {

    }
    void run ()
    {
        for (int i=0;i<DataSize;i++)
        {
            usedBytes.acquire();
            fprintf(stderr,"%d" ,buffer[i%BufferSize]);
            if(i%16==0&&i!=0)
                fprintf(stderr,"\n");
            freeBytes.release ();
        }
        fprintf(stderr, "\n");
    }
};


int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    Producer producer;
    Consumer consumer;
    producer.start ();
    consumer.start ();
    producer.wait ();
    consumer.wait ();

    return a.exec ();
}

示例图片:

QT5多线程_第2张图片

线程等待与唤醒

对生产者和消费者问题的另一个解决方法是使用QWaitCondition类,允许线程 在一定条件下唤醒其他线程。

示例代码:

#include 
#include 
#include 
#include 
#include 

const int DataSize=1000;
const int BufferSize=80;
int buffer[BufferSize];
QWaitCondition bufferEmpty;
QWaitCondition bufferFull;
QMutex mutex;
int numUsedBytes = 0;
int rIndex=0;

class Producer : public QThread
{
public:
    Producer()
    {

    }
    void run()
    {
        for (int i=0; i < DataSize; i++)
        {
            mutex.lock();
            if(numUsedBytes==BufferSize)
                bufferEmpty.wait(&mutex);
            buffer[i%BufferSize]=numUsedBytes;
            ++numUsedBytes;
            bufferFull.wakeAll();
            mutex.unlock();
        }
    }

};

class Consumer : public QThread
{
public:
    Consumer()
    {

    }
    void run ()
    {
        forever
        {
            mutex.lock();
            if(numUsedBytes==0)
                bufferFull.wait(&mutex);
            printf("%ul::[%d]=%d\n",currentThreadId(),rIndex,buffer[rIndex]);
            rIndex=(++rIndex)%BufferSize;
            --numUsedBytes;
            bufferEmpty.wakeAll();
            mutex.unlock();
        }
        printf("\n");
    }
};


int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    Producer producer;
    Consumer consumerA;
    Consumer consumerB;
    producer.start ();
    consumerA.start ();
    consumerB.start ();
    producer.wait ();
    consumerA.wait();
    consumerB.wait ();
    return a.exec ();
}

示例图片:

QT5多线程_第3张图片

注意

后续会在日常专栏中更新对应的应用代码,请注意日常专栏的更新。

你可能感兴趣的:(QT应用开发,qt,c++)