Qt 学习 第4节 多线程

来源:互联网 发布:rna seq数据分析流程 编辑:程序博客网 时间:2024/06/07 03:35
    
回顾Qt之线程(QThread),里面讲解了如何使用线程,但还有很多人留言没有看明白,那么今天我们来一起瞅瞅关于QThread管理线程的那些事儿。。。
一、线程管理

1线程启动

void start(Priority priority = InheritPriority)

调用后会执行run()函数,但在run()函数执行前会发射信号started(),操作系统将根据优先级参数调度线程。如果线程已经在运行,那么这个函数什么也不做。优先级参数的效果取决于操作系统的调度策略。特别是那些不支持线程优先级的系统优先级将会被忽略(例如在Linux中,更多细节请参考http://linux.die.net/man/2/sched_setscheduler)。

2线程执行

int exec()

进入事件循环并等待直到调用exit(),返回值是通过调用exit()来获得,如果调用成功则范围0。

virtual void run();

线程的起点,在调用start()之后,新创建的线程就会调用这个函数,默认实现调用exec(),大多数需要重新实现这个功能,便于管理自己的线程。该方法返回时,该线程的执行将结束

3线程退出

void quit()

告诉线程事件循环退出,返回0表示成功,相当于调用了QThread::exit(0)。

void exit(int returnCode = 0)

告诉线程事件循环退出。

调用这个函数后,线程离开事件循环后返回,QEventLoop::exec()返回returnCode

按照惯例0表示成功,任何非0值表示失败。

void terminate()

终止线程,线程可能会立即被终止也可能不会,这取决于操作系统的调度策略,使用terminate()之后再使用QThread::wait()确保万无一失。

当线程被终止后,所有等待中的线程将会被唤醒。

警告:此功能比较危险,不鼓励使用。线程可以在代码执行的任何点被终止。线程可能在更新数据时被终止,从而没有机会来清理自己,解锁等等。。。总之,只有在绝对必要时使用此功能。

建议:一般情况下,都在run函数里面设置一个标识符,可以控制循环停止。然后才调用quit函数,退出线程。

4线程等待

void msleep(unsigned long msecs)

强制当前线程睡眠msecs毫秒

void sleep(unsigned long secs)

强制当前线程睡眠secs

void usleep(unsigned long usecs)

强制当前线程睡眠usecs

bool wait(unsigned long time = ULONG_MAX);

线程将会被阻塞,等待time毫秒。和sleep不同的是,如果线程退出,wait会返回。

5线程状态

    bool isFinished() const    

    线程是否结束

    bool isRunning() const    
    线程是否正在运行

6线程优先级

    void setPriority(Priority priority)
    这个函数设置正在运行线程的优先级。如果线程没有运行,此功能不执行任何操作并立即返回。使用的start()来启动一个线程具有特定的优先级。
    优先级参数可以是QThread::Priority枚举除InheritPriortyd的任何值。
    Priority priority() const
    下面来看下优先级中的各个枚举值:

Constant

Value

Description

QThread::IdlePriority

0

没有其它线程运行时才调度.

QThread::LowestPriority

1

比LowPriority调度频率低.

QThread::LowPriority

2

比NormalPriority调度频率低.

QThread::NormalPriority

3

操作系统默认的默认优先级.

QThread::HighPriority

4

比NormalPriority调度频繁.

QThread::HighestPriority

5

比HighPriority调度频繁.

QThread::TimeCriticalPriority

6

尽可能频繁的调度.

QThread::InheritPriority

7

使用和创建线程同样的优先级. 这是默认值.

二、主线程、次线程

在Qt之线程(QThread)一节中我介绍了QThread 的两种使用方法:

1、子类化 QThread(不使用事件循环)

这是官方手册、例子以及相关书籍中都介绍的一种常用的方法。

a. 子类化 QThread

b. 重载 run 函数,run函数内有一个while或for的死循环(模拟耗时操作)

c. 设置一个标记为来控制死循环的退出。

2、子类化 QObject

a. 子类化 QObject

b定义槽函数

c. 将该子类的对象moveToThread到新线程中

run 对于线程的作用相当于main函数对于应用程序。它是线程的入口,run的开始和结束意味着线程的开始和结束。

采用这两种做法,毫无疑问都会在次线程中运行(这里说的是,run中的逻辑以及子类化QObject后连接通过moveToThread然后连接到QThread的started()信号的槽函数,这个下面会详细讲解)。

那么,线程中的槽函数是怎么运行的呢?

说到信号与槽,大家应该再熟悉不过了,包括我,特别喜欢使用自定义信号与槽,感觉用起来特方便、特棒。。。

经常使用,你能否100%的使用正确?你了解它的高级用法吗?

1、你是否在多次connect,还发现不了为什么槽函数会执行那N多次。

2、你是否了解disconnect

3、你是否了解connect中的第五个参数 Qt::ConnectionType

关于connect、disconnect信号、槽的使用可参考:Qt之信号与槽。既然谈到线程这里需要重点说下Qt::ConnectionType(信号与槽的传递方式)

Constant

Value

Description

Qt::AutoConnection

0

自动连接:(默认值)如果信号在接收者所依附的线程内发射,则等同于直接连接。如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接。

Qt::DirectConnection

1

直接连接:当信号发射时,槽函数将直接被调用。无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。

Qt::QueuedConnection

2

队列连接:当控制权回到接受者所依附线程的事件循环时,槽函数被调用。槽函数在接收者所依附线程执行。也就是说:这种方式既可以在线程内传递消息,也可以跨线程传递消息

Qt::BlockingQueuedConnection

3

与Qt::QueuedConnection类似,但是会阻塞等到关联的slot都被执行。这里出现了阻塞这个词,说明它是专门用来多线程间传递消息的。

举例:

MyObject.h
#ifndef MYOBJECT_H#define MYOBJECT_H#include class MyObject : public QObject{    Q_OBJECTpublic:    explicit MyObject(QObject *parent = 0);public slots:    void start();};#endif // MYOBJECT_H
MyObject.cpp
#include "MyObject.h"#include #include MyObject::MyObject(QObject *parent)    : QObject(parent){}void MyObject::start(){    qDebug() << QString("my object thread id:") << QThread::currentThreadId();}
main.cpp
#include "MyObject.h"#include #include #include int main(int argc, char *argv[]){    QApplication a(argc, argv);    qDebug() << QString("main thread id:") << QThread::currentThreadId();    MyObject object;    QThread thread;    object.moveToThread(&thread);    QObject::connect(&thread, SIGNAL(started()), &object, SLOT(start()));    thread.start();    return a.exec();} 
查看运行结果:

  "main thread id:" 0xf08

  "my object thread id:" 0x216c

    显然主线程与槽函数的线程是不同的(你可以多次尝试,屡试不爽。。。),因为moveToThread后MyObject所在的线程为QThread,继上面介绍的thread.start()执行后首先会发射started()信号,也就是说started()信号发射是在线程中进行的,所以无论采取Qt::AutoConnectionQt::DirectConnection、Qt::QueuedConnection哪种连接方式,主线程与槽函数的线程都是不同的。

 

1、修改代码如下:

    MyObject object;    QThread thread;    //object.moveToThread(&thread);    QObject::connect(&thread, SIGNAL(started()), &object, SLOT(start()), Qt::DirectConnection);    thread.start();
查看运行结果:

  "main thread id:" 0x2688

  "my object thread id:" 0x2110 

    显然主线程与槽函数的线程是不同的,MyObject所依附的线程为主线程(因为注释掉了moveToThread,继上面介绍的Qt::DirectConnection无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行)。也就是说started()信号发射是在次线程中进行的,槽函数也是在次线程中进行的,所以主线程与槽函数的线程是不同的。


2、修改代码如下:

    MyObject object;    QThread thread;    //object.moveToThread(&thread);    QObject::connect(&thread, SIGNAL(started()), &object, SLOT(start()), Qt::QueuedConnection);    thread.start();

查看运行结果:

  "main thread id:" 0x24ec

  "my object thread id:" 0x24ec 

    显然主线程与槽函数的线程是相同的,继上面介绍的Qt::QueuedConnection槽函数在接收者所依附线程执行)。也就是说started()信号发射是在次线程中进行的,但MyObject所依附的线程为主线程(因为注释掉了moveToThread),所以主线程与槽函数的线程必然是相同的。


3、修改代码如下:

    MyObject object;    QThread thread;    //object.moveToThread(&thread);    QObject::connect(&thread, SIGNAL(started()), &object, SLOT(start()), Qt::AutoConnection);    thread.start();
查看运行结果:

  "main thread id:" 0x2700

  "my object thread id:" 0x2700 

    显然主线程与槽函数的线程是相同的,MyObject所依附的线程为主线程(因为注释掉了moveToThread,继上面介绍的Qt::AutoConnection如果信号在接收者所依附的线程内发射,则等同于直接连接。如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接。)。因为started()信号和MyObject依附的线程不同,所以结果和Qt::QueuedConnection对应的相同,所以主线程与槽函数的线程是相同的。


    基本就介绍到这里,QThread使用和上面的大同小异,run里面执行的代码都是在次线程中,如果是QThead的槽函数,那么结论同上!

this->serverDevice          = new tcpServerDevice();
    QThread *thread_server      = new QThread();
    connect(this->serverDevice,SIGNAL(destroyed(QObject*)),thread_server,SLOT(quit()));
    connect(thread_server,SIGNAL(finished()),thread_server,SLOT(deleteLater()));
    connect(this->serverDevice,SIGNAL(signal_client_newconnect_to_ui(int,QString,quint16)),SLOT(newClientConnet_solt(int,QString,quint16)));
    connect(this->serverDevice,SIGNAL(signal_client_disconnect_to_ui(int)),this,SLOT(slot_list_clientfd_remove(int)));
    connect(this->serverDevice,SIGNAL(signal_send_data_to_ui(int,QByteArray)),this,SLOT(receiveDatFromDevice_slot(int,QByteArray)));
    connect(this,SIGNAL(signal_open_server()),this->serverDevice,SLOT(slot_open_server()));
    connect(this,SIGNAL(signal_close_server()),this->serverDevice,SLOT(slot_close_server()));
    connect(this,SIGNAL(sendDataToDevice_signal(int,QByteArray)),this->serverDevice,SLOT(slot_send_data_to_client(int,QByteArray)));
    this->serverDevice->moveToThread(thread_server);
    thread_server->start();

第一部分:QT线程池的构建与使用

    网上关于QT线程池QThreadPool的文章很多,而且大都千篇一律,基本上都是参考QT的帮助文档介绍QT全局线程池的用法。这样就往往会使人产生误解,QT是不是推荐大家使用其全局线程池,而不推荐使用自定义构造的线程池? 实际情况并不是这样的。而且在实际的项目当中我们通常并不希望仅仅使用一个全局的线程池,而是在需要线程池的工程中都构建和维护自己一个小小的线程池(我们知道一个良好架构的项目通常是由多个工程组成的)。综上,我们来分析以下两个问题:

    (1)    非全局的线程池如果构建与使用呢?

[cpp]view plain copy
 print?
  1. #include <QObject>  
  2. #include <QRunnable>  
  3. #include <QThread>  
  4. #include <QThreadPool>  
  5. #include <QDebug>  
  6. class HelloWorldTask : public QRunnable  
  7. {  
  8.      // 线程执行任务:每间隔1s打印出线程的信息  
  9.     void run()  
  10.     {  
  11.         for (int nCount = 0; nCount < 5; nCount++)  
  12.         {  
  13.         qDebug() << QThread::currentThread();  
  14.         QThread::msleep(1000);  
  15.         }  
  16.     }  
  17. };  
  18. int main(int argc, char *argv[])  
  19. {  
  20.     QCoreApplication a(argc, argv);  
  21.   
  22.     QThreadPool   threadpool;              // 构建一个本地的线程池  
  23.     threadpool.setMaxThreadCount(3);         // 线程池中最大的线程数  
  24.         for (int nNum = 0; nNum < 100; nNum++)  
  25.     {  
  26.         HelloWorldTask   *task;    // 循环构建可在线程池中运行的任务  
  27.         threadpool.start(task);      //线程池分配一个线程运行该任务  
  28.         QThread::msleep(1000);  
  29.     }  
  30.     return a.exec();  
  31. }  

    上述程序,构建了一个线程最大数量为3的本地线程池。每间隔1s的时间创建一个线程任务并置入到线程池的任务队列中(QT内部机制实现该队列,我们只需要调用QThreadPool的start函数置入即可)。每个线程任务的持续时间为5s。

    (2)    程序当中QRunnable是以指针的形式创建的,该指针是需要程序员去释放,还是QThreadPool在运行完线程后自动释放?

    解答:在上述例子当中,我们创建的QRunnable类型的指针 QRunnable *task 是不需要我们手动去回收内存的,QThreadPool在结束该任务的执行后会将对该内存进行清空。

    上述解答并不是凭空猜测,一方面根据是QT文档中的一句话:

    QThreadPool takes ownership and deletes 'hello'automatically

     用直白的话说就是:QThreadPool会占有这个指针的句柄并在运行结束后释放指针所占的内存。

    另一方面,我们也通过改进上面的例子进行验证。

[cpp]view plain copy
 print?
  1. #include <QObject>  
  2. #include <QRunnable>  
  3. #include <QThread>  
  4. #include <QThreadPool>  
  5. #include <QDebug>  
  6. class HelloWorldTask : public QRunnable  
  7. {  
  8.      // 线程执行任务:每间隔1s打印出线程的信息  
  9.     void run()  
  10.     {  
  11.             int         m_dataMem[256*1000];                  // 占约 1MB内存空间   
  12.         for (int nCount = 0; nCount < 5; nCount++)  
  13.         {  
  14.         qDebug() << QThread::currentThread();  
  15.         QThread::msleep(1000);  
  16.         }  
  17.     }  
  18. };  
  19. int main(int argc, char *argv[])  
  20. {  
  21.     QCoreApplication a(argc, argv);  
  22.   
  23.     QThreadPool   threadpool;   
  24.     threadpool.setMaxThreadCount(1);   
  25.         for (int nNum = 0; nNum < 100; nNum++)  
  26.     {  
  27.         HelloWorldTask   *task;   
  28.         threadpool.start(task);   
  29.         QThread::msleep(1000);  
  30.     }  
  31.     return a.exec();  
  32. }  

     在程序运行过程中,我们观察发现程序的进程一直仅占用约1MB的内存空间。如果在main函数中所创建的100个HelloWorldTask 指针对象没有被QThreadPool释放的话,随着程序的运行该程序所占内存空间应该逐步攀升到约100MB。然而实际情况是,该程序最高仅占用1MB的内存空间。

    综上两个方面可以得出以下结论:QRunnable创建的对象QThreadPool在执行完该对象后会帮助我们来清空内存,不需要我们手动回收内存。

第二部分:QThread 与QRunnable + QThreadPool适用的应用场景

    QThread是QT的线程类,通过继承QThread然后重写run函数即可实现一个线程类。QThreadPool+ QRunnable配合构建线程池的方法也可以实现线程。我们通过以下问题对上述两种构建线程的方法进行分析和说明。

    (1)既然QThread这么简单我们为什么还要使用QThreadPool + QRunnable创建线程池的方法来使用线程机制呢?

主要原因:当线程任务量非常大的时候,如果频繁的创建和释放QThread会带来比较大的内存开销,而线程池则可以有效避免该问题,相关的基础支持可以自行百度线程池的优点。

    (2)QThread与 QThreadPool + QRunnable分别适用于哪种应用场景?

     QThread适用于那些常驻内存的任务。而且QThread可以通过信号/槽的方式与外界进行通信。而QRunnable则适用于那些不常驻内存,任务数量比较多的情况。

 

第三部分:QRunnable 如何与外界进行通信

       方法1:QRunnable并不继承自QObject类,因此无法使用信号/槽的方式与外界进行通信。我们就必须的使用其他方法,这里给大家介绍的是使用:QMetaObject::invokeMethod()函数。

       方法2:使用多重继承的方法,任务子类同时继承自QRunnable和QObject。

【Qt开发】QThread中的互斥、读写锁、信号量、条件变量

在gemfield的《从pthread到QThread》一文中我们了解了线程的基本使用,但是有一大部分的内容当时说要放到这片文章里讨论,那就是线程的同步问题。关于这个问题,gemfield在《从进 程到线程》中有一个比喻,有必要重新放在下面温习下:

*******************************
最后用一个比喻来总结下:
1、一个进程就好比一个房子里有一个人;
2、clone创建线程就相当于在这个房子里添加一个人;
3、fork创建进程就相当于再造一个房子,然后在新房子里添加一个人;

有了上面的比喻后,我们就清楚很多了:
1、线程之间有很多资源可以共享:比如厨房资源、洗手间资源、热水器资源等;
2、而对于进程来说,一个概念就是进程间通信(你要和另外一个房子里的人通信要比一个房子里的两个人之间通信复杂);
3、线程之间因为共享内存,所以通过一个全局的变量就可以交换数据了;
4、但与此同时,对于线程来说,又有新的概念产生了:
a、一个人使用洗手间的时候,得锁上以防止另一个人对洗手间的访问;
b、一个人(或几个人)睡觉的时候,另外一个人可以按照之前约定的方式来叫醒他;
c、热水器的电源要一直开着,直到想洗澡的人数减为0;

上面的概念,在gemfield的后文中术语化的时候,你就不会再觉得很深奥或者枯燥了。
********************************
对于上面的a:一个人使用洗手间的时候,得锁上以防止另一个人对洗手间的访问。我们在QThread里使用的就是QMutext这个互斥了。mutex是mutual exclusion(互相排斥)的简写。在pthread中也有pthread_mutex_*族,但是在QThread中我们能在Qt的框架下通过源代码看到具体实现,所以pthread_mutex_*就靠你自行研究了。

第一部分、QMutex的研究

1、来一小段代码背景:
************************
int number = 6;

void gemfield1()
{
number *= 5;
number /= 4;
}

void gemfield2()
{
number *= 3;
number /= 2;
}
**************************
如果下面的代码是顺序执行的,则会有下面这样的输出逻辑:
**************************
// gemfield1()
number *= 5; // number 为 30
number /= 4; // number 为 7

// gemfield2()
number *= 3; // number 为 21
number /= 2; // number 为 10
**************************
但如果是在2个线程中(线程1、线程2)分别同时调用了gemfield1()、gemfield2()呢?
**************************
// 线程1调用gemfield1()
number *= 5; // number 为30

// 线程2 调用 gemfield2().
// 线程1 被系统调度出去了,而把线程2调度进来运行
number *= 3; // number 为90
number /= 2; // number 为45

// 线程1 结束运行
number /= 4; // number 为11, 而不是上面的10
**************************

2、如何解决这个问题?

很明显我们想要一个线程(比如线程1)在访问变量number的时候,除非该线程(比如线程1)允许,否则其他线程(比如线程2)不能访问number;这就好比一个人访问洗手间,另一个人就无法访问一样(我们把对number的访问区域,或者洗手间这个区域称作临界区域);下面就是QMutex的使用:
***************************
QMutex mutex;
int number = 6;

void gemfield1()
{
mutex.lock();
number *= 5;
number /= 4;
mutex.unlock();
}

void gemfield2()
{
mutex.lock();
number *= 3;
number /= 2;
mutex.unlock();
}
****************************
当mutex这个互斥lock上之后,直到unlock之前,都只有1个线程访问number;注意:mutex变量和number一样是全局变量!

在QMutex的使用中,我们关注以下4个方法和2个属性:

1、QMutex ()//构造1个mutex
2、lock ()//锁
3、tryLock ()//尝试着锁
4、unlock ()//释放锁

另外两个属性是:递归和非递归。如果这个mutex是递归的话,表明它可以被一个线程锁多次,也就是锁和解锁中再嵌套锁和解锁;非递归的话,就表明mutex只能被锁一次。

这四个的用法已经在上面的代码中展示过了,现在来看看QMutex是怎么做到这一点的?

3、QMutex是如何做到保护临界区域的?

设想一下我们的洗手间问题:洗手间提供了什么机制,让一个人在使用的时候,另一个人无法闯入?门锁!现在开始我们的QMutex之旅:

a、首先得构造出一个QMutex对象吧,要了解这一点,我们得先了解下QMutex的类型层次及成员。

class QBasicMutex
{
public:
inline void lock() {
if (!fastTryLock())
lockInternal();
}

inline void unlock() {
Q_ASSERT(d_ptr.load()); //mutex must be locked
if (!d_ptr.testAndSetRelease(dummyLocked(), 0))
unlockInternal();
}

bool tryLock(int timeout = 0) {
return fastTryLock() || lockInternal(timeout);
}

private:
inline bool fastTryLock() {
return d_ptr.testAndSetAcquire(0, dummyLocked());
}
bool lockInternal(int timeout = -1);
void unlockInternal();
QBasicAtomicPointer<QMutexData> d_ptr;

static inline QMutexData *dummyLocked() {
return reinterpret_cast<QMutexData *>(quintptr(1));
}

friend class QMutex;
friend class QMutexData;
};
————————————————–
class QMutex : public QBasicMutex {
public:
enum RecursionMode { NonRecursive, Recursive };
explicit QMutex(RecursionMode mode = NonRecursive);
};

————————————————–
class QMutexData
{
public:
bool recursive;
QMutexData(QMutex::RecursionMode mode = QMutex::NonRecursive)
: recursive(mode == QMutex::Recursive) {}
};

————————————————–
class QMutexPrivate : public QMutexData {
public:
QMutexPrivate();
bool wait(int timeout = -1);
void wakeUp();
// Conrol the lifetime of the privates
QAtomicInt refCount;
int id;

bool ref() {
Q_ASSERT(refCount.load() >= 0);
int c;
do {
c = refCount.load();
if (c == 0)
return false;
} while (!refCount.testAndSetRelaxed(c, c + 1));
Q_ASSERT(refCount.load() >= 0);
return true;
}
void deref() {
Q_ASSERT(refCount.load() >= 0);
if (!refCount.deref())
release();
Q_ASSERT(refCount.load() >= 0);
}
void release();
static QMutexPrivate *allocate();
QAtomicInt waiters; //number of thread waiting
QAtomicInt possiblyUnlocked; //bool saying that a timed wait timed out
enum { BigNumber = 0×100000 }; //Must be bigger than the possible number of waiters (number of threads)
void derefWaiters(int value);
bool wakeup;
pthread_mutex_t mutex;
pthread_cond_t cond;
};
———————————-
QMutex的类层次上面已经展现了,我们来看看怎么构造一个QMutex对象吧:

QMutex::QMutex(RecursionMode mode)
{
d_ptr.store(mode == Recursive ? new QRecursiveMutexPrivate : 0);
}
其中的d_ptr是在QBasicMutex中定义的:

QBasicAtomicPointer<QMutexData> d_ptr;
根据QMutex构造时的参数,将QMutexData中的recursive成员赋值:默认是0,也就是QMutex::NonRecursive。

b、使用lock(),那么lock()又是怎么实现的呢?

从上面的类型层次可以看出,这个接口是QBasicMutex类实现的,如下:

inline void lock() {
if (!fastTryLock())
lockInternal();
}

也就是说,必须是fastTryLock()返回值为0才有实际的动作,那fastTryLock()又是什么呢?

inline bool fastTryLock() {
return d_ptr.testAndSetAcquire(0, dummyLocked());
}

testAndSetAcquire()又是什么呢?
****************************************************************************************
原型:bool testAndSetAcquire(T *expectedValue, T *newValue);

对于x86平台来说,实现在arch\qatomic_i386.h中:

template <typename T>
Q_INLINE_TEMPLATE bool QBasicAtomicPointer<T>::testAndSetAcquire(T *expectedValue, T *newValue)
{
return testAndSetOrdered(expectedValue, newValue);
}

testAndSetOrdered(expectedValue, newValue)又是怎样实现的?根平台和编译器相关,对于gemfield本文来说,就是 Linux上的GCC编译器,那么:

template <typename T>
Q_INLINE_TEMPLATE bool QBasicAtomicPointer<T>::testAndSetOrdered(T *expectedValue, T *newValue)
{
unsigned char ret;
asm volatile(“lock\n”
“cmpxchgl %3,%2\n”
“sete %1\n”
: “=a” (newValue), “=qm” (ret), “+m” (_q_value)
: “r” (newValue), “0″ (expectedValue)
: “memory”);
return ret != 0;
}

**************************************************************************************
d_ptr.testAndSetAcquire(0, dummyLocked());的含义就是判断d_ptr当前的值是不是0,如果是0的话,则将dummyLocked()的值赋给d_ptr,并返回真值;否则什么都不做,并返回false。

static inline QMutexData *dummyLocked() {
return reinterpret_cast<QMutexData *>(quintptr(1));
}

对不起了,各位,我刚洗了个澡回来。我发现照这样写下去本文就写不完了。我决定把本文介绍的内容的底层实现部分放在《Qt的原子操作》一文之后,本文从简介绍下互斥、读写锁、条件变量、信号量这些概念及用法。所以,上面红颜色装饰的内容就先不要看了。

第二部分、QMutexLocker的诞生

QMutexLocker相当于QMutex的简化,提供了简化了的互斥上的操作(也即简化了的加锁和解锁)。

QMutex实现的互斥功能用的不是挺好的吗?怎么又出现了一个QMutexLocker?其实不然,观察下面的这个代码:
****************************************************************
int complexFunction(int flag)
{
mutex.lock();
int retVal = 0;

switch (flag) {
case 0:
case 1:
mutex.unlock();
return moreComplexFunction(flag);
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;
}
*******************************************************************
上面的代码真实的揭露了QMutex的无力,因为只要有mutex.lock(),必然要有mutex.unlock(),否则临界区里的资源将再不能被访问;而上面的代码并不能保证QMutex的对象一定会unlock(代码可能从某个地方就走了,再不回来了)。这个时候QMutexLocker就发挥用处了,因为QMutexLocker一定是以函数内部的局部变量的形式出现的,当它的作用域结束的时候,这个互斥就自然unlock了。代码如下:
*******************************************************************
int complexFunction(int flag)
{
QMutexLocker locker(&mutex);//定义的时候就上锁了

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;//超出函数的作用域就解锁了
}
******************************************************************

第三部分:QReadWriteLock的作用

虽然互斥的功能保证了临界区资源的安全,但是在某些方面并不符合实际;比如一般情况下,资源可以被并发读!举个实际的例子:有一本书(比如CivilNet BOOK),当某个人读到一页时,另外一个人(或者多个人)也可以过来读;但是,当1个人往上面写笔记时,其他人不能一起写,而且只有这个人把笔记写完了,再让大家一起看。

QReadWriteLock的作用就是保证各个线程能并发的读某个资源,但是要写的话,就得真的lock了(所以,QReadWriteLock适合大量并发读,偶尔会有写的情况);代码如下:

*************************************************************
QReadWriteLock lock;

void ReaderThread::run()

{

lock.lockForRead();
read_file();
lock.unlock();

}

void WriterThread::run()
{

lock.lockForWrite();
write_file();
lock.unlock();

}
**************************************************************
特别的,对于lock这个全局锁来说:

1、只要有任意一个线程lock.lockForWrite()之后,所有之后的lock.lockForRead()都将会被阻塞;
2、只要有任意一个线程的lock.lockForWrite()动作还在被阻塞着的话,所有之后的lock.lockForRead()都会失败;
3、如果在被阻塞的队列中既有lock.lockForWrite()又有lock.lockForRead(),那么write的优先级比read高,下一个执行的将会是lock.lockForWrite()。

大多数情况下,QReadWriteLock都是QMutex的直接竞争者.和QMutex类似,QReadWriteLock也提供了它的简化类来应付复杂的加锁解锁(也是通过函数作用域的手段),代码如下:
****************************************************************
QReadWriteLock lock;

QByteArray readData()
{
QReadLocker locker(&lock);

return data;
}

void writeData(const QByteArray &data)
{
QWriteLocker locker(&lock);

}
*************************************************************

第四部分:QSemaphore 提供了QMutex的通用情况

反过来,QMutex是QSemaphore的特殊情况,QMutex只能被lock一次,而QSemaphore却可以获得多次;当然了,那是因为Semaphores要保护的资源和mutex保护的不是一类;Semaphores保护的一 般是一堆相同的资源; 比如:
1、mutex保护的像是洗手间这样的,只能供1人使用的资源(不是公共场所的洗手间);
2、Semaphores保护的是像停车场、餐馆这样有很多位子资源的场所;

Semaphores 使用两个基本操作acquire() 和 release():

比如对于一个停车场来说,一般会在停车场的入口用个LED指示牌来指示已使用车位、可用车位等;你要泊车进去,那就要acquire(1)了,这样available()就会减一;如果你开车离开停车场 ,那么就要release(1)了,同时available()就会加一。

让gemfield用代码来演示一个环形缓冲区和其上的信号量(生产-消费模型):

const int DataSize = 1000;//这个店一共要向这个环形桌上供应1000份涮肉
const int BufferSize = 100;//环形自助餐桌上可以最多容纳下100份涮肉
char buffer[BufferSize];//buffer就是这个环形自助餐桌了

QSemaphore freePlace(BufferSize);//freeBytes信号量控制的是没有放涮肉盘子的区域,初始化值是100,很明显,程序刚开始的时候桌子还是空的;
QSemaphore usedPlace;//usedPlace 控制的是已经被使用的位置,也很明显,程序刚开始的时候,还没开始吃呢。

好了,对于饭店配羊肉的服务员来说,
class Producer : public QThread

public:
void run();//重新实施run虚函数
};

void Producer::run()
{
for (int i = 0; i < DataSize; ++i) {
freePlace.acquire();//空白位置减一
buffer[i % BufferSize] = “M”;//放肉(麦当劳 :-) )
usedPlace.release();//已使用的位置加一
}
}

服务员(producer)要生产1000份涮肉( DataSize), 当他要把生产好的一份涮肉往环形桌子上放之前,必须使用freePlace信号量从环形桌上获得一个空地方(一共就100个)。 如果消费 者吃的节奏没跟的上的话,QSemaphore::acquire() 调用可能会被阻塞。

最后,服务员使用usedPlace信号量来释放一个名额。一个“空的位置”被成功的转变为“已被占用的位置”,而这个位置消费者正准备吃。

对于食客(消费者)来说:

class Consumer : public QThread
{
public:
void run();//重新实施run虚函数
};

void Consumer::run()
{
//消费者一共要吃1000份涮肉
for (int i = 0; i < DataSize; ++i) {
usedPlace.acquire();//如果还没有位置被使用(表明没有放肉),阻塞
eat(buffer[i % BufferSize]);
freePlace.release();//吃完后,空白位置可以加一了
}
leaveCanting();
}

在main函数中,gemfield创建了2个线程,并且通过QThread::wait()来确保在程序退出之前,线程都已经执行完了(也即完成了各自的1000次for循环)

int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}

程序是怎么运行的呢?

初始的时候,只有服务员线程可以做任何事; 消费者线程被阻塞了——等待着usedPlace信号量被释放(available()初始值是0);当服务员把第一份涮肉放到桌子上的时候,
freePlace.available() 的值就变为了BufferSize – 1, 并且usedPlace.available() is 1.这时,两个线程都可以工作了: 消费者可以吃这第一份涮肉,并且服务员再生产第二份涮肉;

在一个多处理器的机器上,这个程序将有可能达到基于mutex的程序的2倍快, 因为两个线程可以同时工作在不同的缓冲区上。

第五部分: QWaitCondition,与QSemaphore的竞争

const int DataSize = 1000;//这个店一共要向这个环形桌上供应1000份涮肉
const int BufferSize = 100;//环形自助餐桌上可以最多容纳下100份涮肉
char buffer[BufferSize];

QWaitCondition placeNotEmpty;//当有肉放上来,就发出这个信号
QWaitCondition placeNotFull;//当消费者吃完一份涮肉后发出这个信号
QMutex mutex;
int numUsedPlace = 0;//已经放了肉的位置数
为了同步服务员和消费者, 我们需要2个条件变量和1个mutex。变量解释参考上面的代码注释。让我们看看服务员这个生产者的类:
************************************************
class Producer : public QThread
{
public:
void run();
};
void Producer::run()
{
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == BufferSize)
placeNotFull.wait(&mutex);
mutex.unlock();

buffer[i % BufferSize] = “M”;(又是麦当劳)

mutex.lock();
++numUsedBytes;
placeNotEmpty.wakeAll();
mutex.unlock();
}
}
******************************************************

在服务员将肉放到环形桌上之前,先要检查下桌子是不是放满了。如果满了,服务员就等待placeNotFull条件.

在最后,服务员将numUsedBytes自增1,并且发出bufferNotEmpty条件是真的这个信号,因为numUsedBytes肯定大于0;

注意,QWaitCondition::wait() 函数使用一个mutex作为它的参数,这样做的意义是:mutex刚开始是lock的,然后当这个线程因为placeNotFull.wait(&mutex);而休眠时,这个mutex就会被unlock,而当这个线程被唤醒时,mutex再次被加锁。

另外, 从locked状态到wait状态是原子的,以此来防止竞态条件的发生。

再来看看消费者类:
**************************************************
class Consumer : public QThread
{
public:
void run();
};

void Consumer::run()
{
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == 0)
placeNotEmpty.wait(&mutex);
mutex.unlock();

eat(buffer[i % BufferSize]);

mutex.lock();
–numUsedBytes;
placeNotFull.wakeAll();
mutex.unlock();
}
leaveCanting();
}
***************************************************
代码和服务员的差不多。再来看看main函数:
***************************************************
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}
***************************************************
和信号一节差不多,程序刚开始的时候,只有服务员线程可以做一些事;消费者被阻塞(等肉),直到“位置不为空”信号发出。

在一个多处理器的机器上,这个程序将有可能达到基于mutex的程序的2倍快, 因为两个线程可以同时工作在不同的缓冲区上。

其实,Qt的线程库所包含的内容正是gemfield上一文《从pthread到QThread》中介绍的QThread类,以及QMutexLocker, QReadWriteLock, QSemaphore, QWaitCondition这些类,再外加一个atomic原子操作的内容(这时gemfield下一篇文章的内容哦);了解了这些,我们就可以更加自信的使用Qt的线程库了。

备注:本文属于gemfield的CivilNet博客(http://syszux.com/blog)[Qt乐园]版块,转载此文时,请保证包括备注在内的本文的完整性

原创粉丝点击