Qt探索之路——多线程实现方法

 
通常情况下,应用程序都是在一个线程中执行操作。但是,当调用一个耗时操作(例如,大批量I/O或大量矩阵变换等CPU密集操作)时,用户界面常常会冻结。而使用
多线程可解决这一问题。
多线程具有以下几点优势。
(1)提高应用程序的响应速度。这对于开发图形界面的程序尤为重要,当一个操作耗时很长时,整个系统都会等待这个操作,程序就不能响应键盘、鼠标、菜单等的操
作,而是使用了多线程技术可将耗时长的操作置于一个新的线程,从而避免以上的问题。
(2)使多CPU系统更加有效。当线程数不大于CPU数目时,操作系统可以调度不同的线程运行于不同的CPU上。
(3)改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为独立或半独立的运行部分,这样有利于代码的理解和维护。
多线程程序有以下几个特点。
(1)多线程程序的行为无法预期,当多次执行上述程序时,每一次的运行结构都可能不同。
(2)多线程的执行顺序无法保证,它与操作系统的调度策略和线程优先级等因素有关。
(3)多线程的切换可能发生在任何时刻、任何地点。
(4)多线程对代码的敏感度高,因此对代码的细微修改都可能产生意想不到的结果。
基于以上这些特点,为了有效地使用线程,开发人员必须对其进行控制。
12.1 多线程及简单实例
点击“开始”按钮将启动数个工作线程(工作线程数目由MAXSIZE宏决定),各个线程循环打印数字0-9.直到按下“停止”按钮终止所有线程为止。
运行效果:
 
threaddlg.h
 
#ifndef THREADDLG_H
#define THREADDLG_H

#include <QDialog>
#include <QPushButton>
#include "workthread.h"

#define MAXSIZE 5

class ThreadDlg : public QDialog
{
    Q_OBJECT

public:
    ThreadDlg(QWidget *parent = 0);
    ~ThreadDlg();
private:
    QPushButton *startBtn;
    QPushButton *stopBtn;
    QPushButton *exitBtn;
public slots:
    void slotstart();                   //启动线程槽函数
    void slotstop();                    //终止线程槽函数
private:
    WorkThread *workThread[MAXSIZE];    //指向工作线程的私有指针数组,记录了所启动的全部线程
};

#endif // THREADDLG_H
 
threaddlg.cpp
 
#include "threaddlg.h"
#include <QHBoxLayout>

ThreadDlg::ThreadDlg(QWidget *parent)
    : QDialog(parent)
{
    setWindowTitle(tr("线程"));
    startBtn = new QPushButton(tr("开始"));
    stopBtn = new QPushButton(tr("停止"));
    exitBtn = new QPushButton(tr("退出"));

    QHBoxLayout *mainLayout = new QHBoxLayout(this);
    mainLayout->addWidget (startBtn);
    mainLayout->addWidget (stopBtn);
    mainLayout->addWidget (exitBtn);

    startBtn->setEnabled (true);
    stopBtn->setEnabled (true);

    connect (startBtn, SIGNAL(clicked(bool)), this, SLOT(slotstart()));
    connect (stopBtn, SIGNAL(clicked(bool)), this, SLOT(slotstop()));
    connect (exitBtn, SIGNAL(clicked(bool)), this, SLOT(close()));
}

ThreadDlg::~ThreadDlg()
{

}
/*
 * 这里使用两个循环,目的是为了使新建的线程尽可能开始执行
*/
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);
}
/*
 * workThread[i]->terminate ()、 workThread[i]->wait ():调用QThread基类的terminate()函数,依次终止保存在
 * workThread[]数组中的WorkThread类实例。但是terminate()函数并不会立刻终止这个线程,该线程何时终止取决于操作系统的
 * 调度策略。因此,程序紧挨着调用了QThread基类的wait()函数,它使得线程阻塞等待直到退出或超时。
*/
void ThreadDlg::slotstop ()
{
   if(!startBtn->isEnabled())
   {
       for(int i = 0; i < MAXSIZE; i++)
       {
            workThread[i]->terminate ();
            workThread[i]->wait ();
       }
       startBtn->setEnabled (true);
       stopBtn->setEnabled (false);
   }
}
 
workthread.h
 
#ifndef WORKTHREAD_H
#define WORKTHREAD_H

#include <QThread>

class WorkThread : public QThread
{
public:
    WorkThread();
protected:
    void run();
};

#endif // WORKTHREAD_H
 
workthread.cpp
 
#include "workthread.h"
#include <QDebug>

WorkThread::WorkThread()
{

}

void WorkThread::run ()
{
    while(true)
    {
        for(int i = 0; i < 10; i++)
        {
            qDebug() << i << i << i << i << i << i << i << i << i << i;
        }
    }
}
12.2 多线程控制
本节介绍Qt线程同步互斥控制的基本方法。线程之间存在着互相制约的关系,具体可分为互斥和同步这两种关系。
实现线程的互斥与同步常使用的类有QMutex、QMutexLocker、QReadWriteLocker、QReadLocker、QWriteLocker、QSemaphore和QWaitCondition。
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;
};
这是实现生成从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)”。临界区(代码段)实施对临界资源的操作,
为了阻止问题的产生,一次只能有一个线程进入临界区。通常有关的机制或方法在程序中加上“进入”或“离开”临界区等操作。如果一个线程已经进入某个临界区,则另一
个线程就绝不允许在此刻再进入同一个临界区。
12.2.1 互斥量
1、QMutex类
QMutex类是对互斥量的处理。它被用来保护一段临界区代码,即每次只允许一个线程访问这段代码。
QMutex类的lock()函数用于锁住互斥量。如果互斥量处于解锁状态,则当前线程就会立即抓住并锁定它,否则当前线程就会被阻塞,直到持有这个互斥量的线程对它解
锁。线程调用lock()函数后就会持有这个互斥量,直到调用unlock()操作为止。
QMutex类提供了一个tryLock()函数。如果互斥量已被锁定,则立即返回。
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;
};
运行界面
 
#include <QCoreApplication>
#include <QSemaphore>
#include <QThread>
#include <stdio.h>

const int DataSize = 1000;
const int BufferSize = 80;
/*
 * 首先生产者向buffer中写入数据,直到它到达终点,然后从起点重新开始覆盖已经存在的数据。消费者读取前者生产的数据,在此处每一个
 * int字长都被看成一个资源,实际应用中常会在更大的单位上进行操作,从而减少使用信号量重带来的开销。
*/
int buffer[BufferSize];
/*
 * freeBytes信号量控制可被生产者填充的缓冲区部分,被初始化为BufferSize(80),表示程序一开始有BufferSize个缓冲区单元可
 * 被填充。
*/
QSemaphore freeBytes(BufferSize);
/*
 * usedBytes信号量控制可被消费者读取的缓冲区部分,被初始化为0,表示程序一开始时缓冲区没有数据可供读取。
*/
QSemaphore usedBytes(0);

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

Producer::Producer()
{

}

/*
 * freeBytes.acquire ():生产者线程首先获取一个空闲单元,如果此时缓冲区被消费者尚未读取的数据填满,对此函数的调用就会阻塞
 * 直到消费者读取了这些数据为止。acquire(n)函数用于获取n个资源,当没有足够的资源时,调用则将被阻塞,直到有足够的可用资源为止
 * 除此之外,QSemaphore类还提供了一个tryAcquire(n)函数,在没有足够的资源时,该函数会立即返回。
 *
*/
void Producer::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();
};

Consumer::Consumer()
{

}
/*
 * usedBytes.acquire ():消费者线程首先获取一个可被读取的单元,如果缓冲区中没有包含任何可以读取的数据,对此函数的调用就会
 * 阻碍,直到生产了一些数据为止。
 * freeBytes.release ():调用该函数使得这个单元变为空闲,以备生产者下次填充
*/
void Consumer::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 p;
    Consumer c;
    /*启动生产者消费者线程*/
    p.start ();
    c.start ();
    
    /*等待生产者和消费者各自执行完毕后自动退出*/
    p.wait ();
    c.wait ();

    return a.exec();
}
 
12.2.3 线程等待与唤醒
对生产者和消费者问题的另一个解决办法是使用QWaitCondition类,允许线程在一定条件下唤醒其他线程。
#include <QCoreApplication>
#include <stdio.h>
#include <QThread>
#include <QWaitCondition>
#include <QMutex>

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

QWaitCondition bufferEmpty;
QWaitCondition bufferFull;

QMutex mutex;
int numUseBytes = 0;
int rIndex = 0;

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

Producer::Producer()
{

}
/*
 * for(int i = 0; i < DataSize; i++){mutex.lock (); ... mutex.unlock ();}
 * for循环中所有语句都需要使用互斥量加以保护,以保证其操作的原子性
 * if(numUseBytes == BufferSize: 首先检查缓冲区是否已经填满
 * bufferEmpty.wait (&mutex):如果缓冲区已经填满,则等待“缓冲区有空位”条件成立。wait()函数将互斥量解锁并在此等待
 * buffer[i % BufferSize] = numUseBytes:如果缓冲区未填满,则向缓冲区中写入一个整数值
 * bufferFull.wakeAll ():最后唤醒等待"缓冲区有可用数据"(bufferEmpty变量)条件为“真”的线程
 * wakeOne()函数在条件满足时随机唤醒一个等待线程,而wakeAll()函数则在条件满足时唤醒所有等待线程。
*/
void Producer::run ()
{
    for(int i = 0; i < DataSize; i++)
    {
        mutex.lock ();
        if(numUseBytes == BufferSize)
        {
            bufferEmpty.wait (&mutex);
        }
        buffer[i % BufferSize] = numUseBytes;
        ++numUseBytes;
        bufferFull.wakeAll ();
        mutex.unlock ();
    }
}

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

Consumer::Consumer()
{

}
/*
 * bufferFull.wait (&mutex):当缓冲区无数据时,等待“缓冲区有可用数据”条件成立
 * bufferEmpty.wakeAll ():唤醒等待“缓冲区有空位”条件的生产者线程
*/
void Consumer::run()
{
    forever
    {
        mutex.lock ();

        if(numUseBytes == 0)
        {
            bufferFull.wait (&mutex);
        }
        printf ("%ul::[%d] = %d\n", currentThreadId (), rIndex, buffer[rIndex]);

        rIndex = (++rIndex) % BufferSize;
        --numUseBytes;
        bufferEmpty.wakeAll ();
        mutex.unlock ();
    }
    printf ("\n");
}

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

    Producer P;
    Consumer CA;
    Consumer CB;

    P.start ();
    CA.start ();
    CB.start ();

    P.wait ();
    CA.wait ();
    CB.wait ();

    return a.exec();
}
12.3多线程应用
本节中通过实现一个多线程的网络时间服务器,介绍如何综合运用多线程技术编程。每当客户请求到达时,服务器将启动一个新线程为它返回当前的时间,服务完毕
后这个线程将自动退出。同时,用户界面会显示当前已接收请求的次数。
12.3.1夫服务器编程
dialog.h
 
#ifndef DIALOG_H
#define DIALOG_H

#include <QDialog>
#include <QLabel>
#include <QPushButton>

class TimeServer;
class Dialog : public QDialog
{
    Q_OBJECT

public:
    Dialog(QWidget *parent = 0);
    ~Dialog();
public slots:
    void slotShow();
private:
    QLabel *label1;
    QLabel *label2;
    QPushButton *quitBtn;
    TimeServer *timeServer;
    int count;

};

#endif // DIALOG_H
 
dialog.cpp
 
#include "dialog.h"
#include <QGridLayout>
#include <QMessageBox>
#include "timeserver.h"

Dialog::Dialog(QWidget *parent)
    : QDialog(parent)
{
    setWindowTitle (tr("多线程时间服务器"));
    label1 = new QLabel(tr("服务器端口:"));
    label2 = new QLabel;
    quitBtn = new QPushButton(tr("退出"));

    QHBoxLayout *HBoxLayout = new QHBoxLayout;
    HBoxLayout->addStretch (1);
    HBoxLayout->addWidget (quitBtn);
    HBoxLayout->addStretch (1);

    QVBoxLayout *mainLayout = new QVBoxLayout(this);
    mainLayout->addWidget (label1);
    mainLayout->addWidget (label2);
    mainLayout->addLayout (HBoxLayout);
    connect (quitBtn, SIGNAL(clicked(bool)), this, SLOT(close()));

    count = 0;
    timeServer = new TimeServer(this);
    if(!timeServer->listen())
    {
        QMessageBox::critical(this,tr("多线程时间服务器"),
        tr("无法启动服务器:%1.").arg(timeServer->errorString()));
        close();
        return;
    }
    label1->setText(tr("服务器端口:%1.").arg(timeServer->serverPort()));

}

Dialog::~Dialog()
{

}

void Dialog::slotShow ()
{
    label2->setText (tr("服务器端口:%1.").arg(++count));
}
 
timeserver.h
 
#ifndef TIMERSERVER_H
#define TIMERSERVER_H

#include <QTcpServer>

class Dialog;               //服务器端口声明

class TimeServer : public QTcpServer
{
    Q_OBJECT

public:
    TimeServer(QObject *parent = 0);
protected:
    void incomingConnection (int socketDescriptor);
private:
    Dialog *dlg;
};

#endif // TIMERSERVER_H
 
timeserver.cpp
 
#include "timeserver.h"
#include "timethread.h"
#include "dialog.h"

/*
 * Dialog *dlg:用于记录创建这个TCP服务器端对象的父类,这里是界面指针,通过这个指针将线程发出的消息关联到界面的槽函数中
*/
TimeServer::TimeServer(QObject *parent)
    :QTcpServer(parent)
{
    dlg = (Dialog *)parent;
}
/*
 * 重写此数函数。这个函数在TCP服务器端有新的连接时被调用,其参数为所接收新连接的套接字描述符。
 *
*/
void TimeServer::incomingConnection (int socketDescriptor)
{
    TimeThread *thread = new TimeThread(socketDescriptor, 0);
    connect (thread, SIGNAL(finished()), dlg, SLOT(slotShow()));
    connect (thread, SIGNAL(finished()), thread, SLOT(deleteLater()), Qt::DirectConnection);
    thread->start ();
}

timethread.h
 
#ifndef TIMETHREAD_H
#define TIMETHREAD_H

#include <QThread>
#include <QtNetwork>
#include <QTcpSocket>

class TimeThread : public QThread
{
    Q_OBJECT
public:
    TimeThread(int socketDescriptor,QObject *parent=0);
    void run();
signals:
    void error(QTcpSocket::SocketError socketError);
private:
    int socketDescriptor;
};

#endif // TIMETHREAD_H
 
timethread.cpp
 
#include "timethread.h"
#include <QDateTime>
#include <QByteArray>
#include <QDataStream>

TimeThread::TimeThread(int socketDescriptor, QObject *parent)
    :QThread(parent), socketDescriptor(socketDescriptor)
{

}
/*
 * TimeThread::run ()函数是工作线程(TimeThread)的实质所在,当在TimeServer::incomingConnection()函数中调用了
 * thread->start()函数后,次数函数开始执行
*/
void TimeThread::run ()
{
    QTcpSocket tcpSocket;   //创建一个QTcpSocket类
    /*将以上创建的QTcpSocket类置以从构造函数中传入的套接字描述符,用于向客服端传回服务器端的当前时间*/
    if(!tcpSocket.setSocketDescriptor (socketDescriptor))
    {
        emit error (tcpSocket.error ());
        return;
    }

    QByteArray block;
    QDataStream out(&block, QIODevice::WriteOnly);
    out.setVersion (QDataStream::Qt_4_3);
    uint time2u = QDateTime::currentDateTime ().toTime_t ();
    out << time2u;
    tcpSocket.write (block);            //将获得当前时间传回客户端
    tcpSocket.disconnectFromHost ();    //端口连接
    tcpSocket.waitForDisconnected ();   //等待返回
}

12.3.2 客户端编程
运行界面如下
 
timeclient.h
 
#ifndef TIMECLIENT_H
#define TIMECLIENT_H

#include <QDialog>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QDateTimeEdit>
#include <QTcpSocket>
#include <QAbstractSocket>

class TimeClient : public QDialog
{
    Q_OBJECT

public:
    TimeClient(QWidget *parent = 0);
    ~TimeClient();
public slots:
    void enableGetBtn();
    void getTime();
    void readTime();
    void showError(QAbstractSocket::SocketError socketError);
private:
    QLabel *serverNameLabel;
    QLineEdit *serverNameLineEdit;
    QLabel *portLabel;
    QLineEdit *portLineEdit;
    QDateTimeEdit *dataTimeEdit;
    QLabel *stateLabel;
    QPushButton *getBtn;
    QPushButton *quitBtn;
    uint time2u;
    QTcpSocket *tcpsocket;
};

#endif // TIMECLIENT_H
 
timeclient.cpp
 
#include "timeclient.h"
#include <QGridLayout>
#include <QMessageBox>

TimeClient::TimeClient(QWidget *parent)
    : QDialog(parent)
{
    setWindowTitle (tr("多线程服务客户端"));
    serverNameLabel = new QLabel(tr("服务器名:"));
    serverNameLineEdit = new QLineEdit("Localhost");

    portLabel = new QLabel(tr("端口:"));
    portLineEdit = new QLineEdit;

    QGridLayout *layout = new QGridLayout;
    layout->addWidget (serverNameLabel, 0, 0);
    layout->addWidget (serverNameLineEdit, 0, 1);
    layout->addWidget (portLabel, 1, 0);
    layout->addWidget (portLineEdit, 1, 1);

    dataTimeEdit = new QDateTimeEdit(this);
    QHBoxLayout *layout1 = new QHBoxLayout;
    layout1->addWidget (dataTimeEdit);

    stateLabel = new QLabel(tr("请首先运行时间服务器!"));
    QHBoxLayout *layout2 = new QHBoxLayout;
    layout2->addWidget (stateLabel);

    getBtn = new QPushButton(tr("获得时间"));
    getBtn->setDefault (true);
    getBtn->setEnabled (false);
    quitBtn = new QPushButton(tr("退出"));
    QHBoxLayout *layout3 = new QHBoxLayout;
    layout3->addStretch (1);
    layout3->addWidget (getBtn);
    layout3->addWidget (quitBtn);

    QVBoxLayout *mainLayout = new QVBoxLayout(this);

    mainLayout->addLayout (layout);
    mainLayout->addLayout (layout1);
    mainLayout->addLayout (layout2);
    mainLayout->addLayout (layout3);

    connect (serverNameLineEdit, SIGNAL(textChanged(QString)), this, SLOT(enableGetBtn()));
    connect (portLineEdit, SIGNAL(textChanged(QString)), this, SLOT(enableGetBtn()));
    connect(getBtn, SIGNAL(clicked(bool)), this, SLOT(getTime()));
    connect(quitBtn, SIGNAL(clicked(bool)), this, SLOT(close()));

    tcpsocket = new QTcpSocket(this);
    connect (tcpsocket, SIGNAL(readyRead()), this, SLOT(getTime()));
    connect (tcpsocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(showError(QAbstractSocket::SocketError)));

    portLineEdit->setFocus ();
}

TimeClient::~TimeClient()
{

}

void TimeClient::enableGetBtn ()
{
    getBtn->setEnabled (!serverNameLineEdit->text ().isEmpty () &&
                        !portLineEdit->text ().isEmpty ());
}

void TimeClient::getTime ()
{
    getBtn->setEnabled (false);
    time2u = 0;
    tcpsocket->abort ();
    tcpsocket->connectToHost (serverNameLineEdit->text (), portLineEdit->text ().toInt ());
}

void TimeClient::readTime ()
{
    QDataStream in(tcpsocket);
    in.setVersion (QDataStream::Qt_4_3);
    if(time2u == 0)
    {
        if(tcpsocket->bytesAvailable () < (int)sizeof(uint))
        {
            return;
        }
        in >> time2u;
    }
    dataTimeEdit->setDateTime (QDateTime::fromTime_t (time2u));
    getBtn->setEnabled (true);
}

void TimeClient::showError (QAbstractSocket::SocketError socketError)
{
    switch(socketError)
    {
    case QAbstractSocket::RemoteHostClosedError:
        break;
    case QAbstractSocket::HostNotFoundError:
        QMessageBox::information (this, tr("时间服务客户端"), tr("主机不可达!"));
        break;
    case QAbstractSocket::ConnectionRefusedError:
        QMessageBox::information (this, tr("时间服务客户端"), tr("连接被拒绝"));
        break;
    default:
        QMessageBox::information (this, tr("时间服务客户端"),
                                  tr("产生如下错误:%1.").arg (tcpsocket->errorString ()));
        break;
    }
    getBtn->setEnabled (true);
}
posted @ 2017-01-06 10:34  shawn06  阅读(513)  评论(0编辑  收藏  举报