
1、系统对多线程编程的支持
不同的平台对 Qt 的多线程支持方式是不同的。当用户在 Windows *** 作系统上安装 Qt 系统时,线程支持是编译器的一个选项,在 Qt 的 mkfiles 子目录中包括了不同种类编译器的编译文件,其中带有 -mt 后缀的文件才是支持多线程的。
而在 Unix *** 作系统中,线程的支持是通过在运行 configure 脚本文件时添加 -thread选项加入的。安装过程将创建一个独立的库,即libqt-mt,因此要支持多线程编程时,必须与该库链接(链接选项为-lqt-mt),而不是与通常的 Qt 库(-lqt)链接。
另外,无论是何种平台,在增加线程支持时都需要定义宏QT_THREAD_SUPPORT(即增加编译选项-DQT_THREAD_SUPPORT)。在 Windows *** 作系统中,这一点通常是在qconfig.h 文件中增加一个选项来实现的。而在 Unix 系统中通常添加在有关的 Makefile 文件中。
2、Qt中的线程类
在Qt 系统中与线程相关的最重要的类当然是 QThread 类,该类提供了创建一个新线程以及控制线程运行的各种方法。线程是通过QThread::run() 重载函数开始执行的,这一点很象 Java 语言中的线程类。在 Qt 系统中,始终运行着一个GUI主事件线程,这个主线程从窗口系统中获取事件,并将它们分发到各个组件去处理。在 QThread类中还有一种从非主事件线程中将事件提交给一个对象的方法,也就是 QThread::postEvent()方法,该方法提供了 Qt 中的一种Thread-safe 的事件提交过程。提交的事件被放进一个队列中,然后 GUI主事件线程被唤醒并将此事件发给相应的对象,这个过程与一般的窗口系统事件处理过程是一样的。值得注意的是,当事件处理过程被调用时,是在主事件线程中被调用的,而不是在调用QThread::postEvent 方法的线程中被调用。比如用户可以从一个线程中迫使另一个线程重画指定区域:
QWidget *mywidget
QThread::postEvent(mywidget, new QPaintEvent(QRect(0,0,100,100)))
然而,只有一个线程类是不够的,为编写出支持多线程的程序,还需要实现两个不同的线程对共有数据的互斥访问,因此 Qt 还提供了 QMutex类,一个线程在访问临界数据时,需要加锁,此时其他线程是无法对该临界数据同时加锁的,直到前一个线程释放该临界数据。通过这种方式才能实现对临界数据的原子 *** 作。
除此之外,还需要一些机制使得处于等待状态的线程在特定情况下被唤醒。QWaitCondition 类就提供了这种功能。当发生特定事件时,QWaitCondition 将唤醒等待该事件的所有线程或者唤醒任意一个被选中的线程。
3、用户自定义事件在多线程编程中的应用
在 Qt 系统中,定义了很多种类的事件,如定时器事件、鼠标移动事件、键盘事件、窗口控件事件等。通常,事件都来自底层的窗口系统,Qt 的主事件循环函数从系统的事件队列中获取这些事件,并将它们转换为 QEvent,然后传给相应的 QObjects 对象。
除此之外,为了满足用户的需求,Qt 系统还提供了一个 QCustomEvent 类,用于用户自定义事件,这些自定义事件可以利用QThread::postEvent() 或者QApplication::postEvent() 被发给各种控件或其他 QObject实例,而 QWidget 类的子类可以通过 QWidget::customEvent()事件处理函数方便地接收到这些自定义的事件。需要注意的是:QCustomEvent 对象在创建时都带有一个类型标识 id以定义事件类型,为了避免与 Qt 系统定义的事件类型冲突,该 id 值应该大于枚举类型 QEvent::Type 中给出的 "User" 值。
在下面的例子中,显示了多线程编程中如何利用用户自定义事件类。
UserEvent类是用户自定义的事件类,其事件标识为3467Array8,显然不会与系统定义的事件类型冲突。
class UserEvent : public QCustomEvent //用户自定义的事件类
{
public:
UserEvent(QString s) : QCustomEvent(3467Array8), sz(s) { }
QString str() const { return sz}
private:
QString sz
}
UserThread类是由QThread类继承而来的子类,在该类中除了定义有关的变量和线程控制函数外,最主要的是定义线程的启动函数UserThread::run(),在该函数中创建了一个用户自定义事件UserEvent,并利用QThread类的postEvent函数提交该事件给相应的接收对象。
class UserThread : public QThread //用户定义的线程类
{
public:
UserThread(QObject *r, QMutex *m, QWaitCondition *c)
QObject *receiver
}
void UserThread::run() //线程类启动函数,在该函数中创建了一个用户自定义事件
{UserEvent *re = new UserEvent(resultstring)
QThread::postEvent(receiver, re)
}
UserWidget类是用户定义的用于接收自定义事件的QWidget类的子类,该类利用slotGo()函数创建了一个新的线程recv(UserThread类),当收到相应的自定义事件(即id为3467Array8)时,利用customEvent函数对事件进行处理。
void UserWidget::slotGo()//用户定义控件的成员函数
{ mutex.lock()
if (! recv)
recv = new UserThread(this, &mutex, &condition)
recv->start()
mutex.unlock()
}
void UserWidget::customEvent(QCustomEvent *e) //用户自定义事件处理函数
{ if (e->type()==3467Array8)
{
UserEvent *re = (UserEvent *) e
newstring = re->str()
}
}
在这个例子中,UserWidget对象中创建了新的线程UserThread,用户可以利用这个线程实现一些周期性的处理(如接收底层发来的消息等),一旦满足特定条件就提交一个用户自定义的事件,当UserWidget对象收到该事件时,可以按需求做出相应的处理,而一般情况下,UserWidget对象可以正常地执行某些例行处理,而完全不受底层消息的影响。
4、利用定时器机制实现多线程编程
为了避免Qt系统中多线程编程带来的问题,还可以使用系统中提供的定时器机制来实现类似的功能。定时器机制将并发的事件串行化,简化了对并发事件的处理,从而避免了thread-safe方面问题的出现。
在下面的例子中,同时有若干个对象需要接收底层发来的消息(可以通过Socket、FIFO等进程间通信机制),而消息是随机收到的,需要有一个GUI主线程专门负责接收消息。当收到消息时主线程初始化相应对象使之开始处理,同时返回,这样主线程就可以始终更新界面显示并接收外界发来的消息,达到同时对多个对象的控制;另一方面,各个对象在处理完消息后需要通知GUI主线程。对于这个问题,可以利用第3节中的用户自定义事件的方法,在主线程中安装一个事件过滤器,来捕捉从各个对象中发来的自定义事件,然后发出信号调用主线程中的一个槽函数。
另外,也可以利用Qt中的定时器机制实现类似的功能,而又不必担心Thread-safe问题。下面就是有关的代码部分:
在用户定义的Server类中创建和启动了定时器,并利用connect函数将定时器超时与读取设备文件数据相关联:
Server:: Server(QWidget *parent) : QWidget(parent)
{
readTimer = new QTimer(this) //创建并启动定时器
connect(readTimer, SIGNAL(timeout()), this, SLOT(slotReadFile())) //每当定时器超时时调用函数slotReadFile读取文件
readTimer->start(100)
}
slotReadFile函数负责在定时器超时时,从文件中读取数据,然后重新启动定时器:
int Server::slotReadFile()// 消息读取和处理函数
{
readTimer->stop()//暂时停止定时器计时
ret = read(file, buf ) //读取文件
if(ret == NULL)
{readTimer->start(100)//当没有新消息时,重新启动定时器
return(-1)
}
else
根据buf中的内容将消息分发给各个相应的对象处理……;
readTimer->start(100) //重新启动定时器
}
在该程序中,利用了类似轮循的方式定时对用户指定的设备文件进行读取,根据读到的数据内容将信息发送到各个相应的对象。用户可以在自己的GUI主线程中创建一个Server类,帮助实现底层的消息接收过程,而本身仍然可以处理诸如界面显示的问题。当各个对象完成处理后,通过重新启动定时器继续进行周期性读取底层设备文件的过程。当然,这种方法适合于各对象对事件的处理时间较短,而底层设备发来消息的频率又相对较慢的情况。在这种情况下,上述方法完全可以满足用户的需求,而又避免了处理一些与线程并发有关的复杂问题。
当然,利用定时器机制实现多线程编程在某些方面具有一定的局限性,有关到底如何实现多线程编程,如何编写出效率更高的代码,还有待于开发者进一步研究和探讨。
Qt同步线程的几种方法一、QMutex类
QMutex类就像一把锁,在互斥量之前上锁(QMutex::lock()),然后在使用完互斥量之后解锁(QMutex::unlock())。比如下面的代码:
[cpp] view plain copy
void someMethod()
{
mutex.lock()
qDebug()<<"Hello"
qDebug()<<"World"
mutex.unlock()
}
class Thread1 : public QThread
{
protected:
void run()
{
someMethod()
}
}
class Thread2 : public QThread
{
protected:
void run()
{
someMethod()
}
}
如上面的代码,在函数someMethod里面有两条语句,如果有两个线程启动之后,这两个线程都将调用这个函数(run函数即为线程启动后执行的程序),则可能会出现的结果是Hello Hello World World。但是这并不是我们想要的,我们希望的是每个线程可以一次性执行完someMethod函数里面的代码。这个时候我们便可以在函数俩面给函数体加上锁,然后在结束的时候解锁。
这里需要注意的是,如果一个线程试图向一个已经被其它线程上锁了互斥量上锁的话,这个线程将被阻塞,直到这个互斥量被解锁。如果一个线程希望自己在试图对一个上锁了的互斥量进行访问的时候能够不被阻塞,可以将lock()函数替换为tryLock()函数,这个函数的效果是:如果线程正在试图访问的互斥量已经被上锁了,那么可以立即返回而不被阻塞。
二、QMutexLocker便利类
使用QMutex对互斥量进行加锁解锁比较繁琐,在一些复杂的函数或者抛出C++异常的函数中都非常容易发生错误。可以使用一个方便的QMutexLocker类来简化对互斥量的处理。首先,QMutexLocker类的构造函数接收一个QMutex对象作为参数并且上锁,然后在析构函数中自动对其进行解锁。如下代码:
[cpp] view plain copy
QMutex mutex
void someMethod()
{
QMutexLocker locker(&mutex)
qDebug()<<"Hello"
qDebug()<<"World"
}
这里创建一个QMutexLocker类实例,在这个实例的构造函数中将对mutex对象进行加锁。然后在析构函数中自动对mutex进行解锁。解锁的工作不需要显示地调用unlock函数,而是根据QMutexLocker对象的作用域绑定在一起了。
三、QReadWriteLock类
前两种保护互斥量的方法比较绝对,其达到的效果是:不管我要对互斥量做些是什么,我都要一个人霸占着,即使我只是看看它,也不能让别人看。这会使得这个互斥量资源的使用率大大下降,造成资源等待等问题。于是,我们可以对线程对互斥量的 *** 作进行分类:读和写。有几种情况:1、如果我只是看看的话,你也可以看,大家看到的都是正确的;2、如果我要看这个数据,你是不能改的,不然我看到的就不知道是什么了;3、我在改的时候,你不能看的,我可能会让你看到不正确的了;4、我在改的时候,你当然不能改了。
因此,我们可以对QMutex锁进行升级,将其升级为QReadWriteLock,QMutex加锁的方法是lock(),而QReadWriteLock锁有两种锁法:设置为读锁(lockForRead())和写锁(lockForWrite())。代码如下:
QReadWriteLock lock
void someMethod()
{
lock.lockForRead()
//lock.lockForWrite()
qDebug()<<"Hello"
qDebug()<<"World"
lock.unlock()
}
于是可能有一下三种情况:1、一个线程试图对一个加了读锁的互斥量进行上读锁,允许;2、一个线程试图对一个加了读锁的互斥量进行上写锁,阻塞;3、一个线程试图对一个加了写锁的互斥量进行上读锁,阻塞;一个线程试图对一个加了写锁的互斥量进行上写锁,阻塞。
所以可以看出,读写锁比较适用的情况是:需要多次对共享的数据进行读 *** 作的阅读线程。
四、QReadLocker便利类和QWriteLocker便利类对QReadWriteLock进行加解锁
和QMutex与QMutexLocker类的关系类似,关于读写锁也有两个便利类,读锁和写锁,QReadLocker和QWriteLocker。它们的构造函数都是一个QReadWriteLock对象,不同的是,在QReadLocker的构造函数里面是对读写锁进行lockForRead()加锁 *** 作,而在QWriteLocker的构造函数里面是对读写锁进行lockForWrite()加锁 *** 作。然后解锁 *** 作unlock()都是在析构函数中完成的。
五、信号量QSemaphore
前面的几种锁都是用来保护只有一个量的互斥量的。但是还有些互斥量(资源)的数量并不止一个,比如一个电脑安装了2个打印机,我已经申请了一个,但是我不能霸占这两个,你来访问的时候如果发现还有空闲的仍然可以申请到的。于是这个互斥量可以分为两部分,已使用和未使用。一个线程在申请的时候,会对未使用到的部分进行加锁 *** 作,如果加锁失败则阻塞,如果加锁成功,即又有一个资源被使用了,于是则将已使用到的部分解锁一个。以著名的生产者消费者问题为例,分析问题:生产者需要的是空闲位置存放产品,结果是可取的产品多了一个。于是,我们可以定义两个信号量:QSemaphore freeSpace和QSemaphore usedSpace,前者是给生产者使用的,后者是给
六、条件触发QWaitCondition(未完待续、、、)
Blocking Master 展示了如何在工作线程中使用QSerialPort的同步(synchronous)API为串行接口创建应用程序。
MasterThread是一个QThread子类,提供用于调度对从属服务器的请求的API。 此类提供了用于响应和报告错误的信号。 可以调用 transaction() 方法以使用所需的请求启动新的主事务。 结果由 response() 信号提供。 如果出现任何问题,将发出 error() 或 timeout() 信号。
注意, transaction() 方法是在主线程中调用的,而请求是在MasterThread线程中提供的。 MasterThread数据成员在不同的线程中并发读取和写入,因此 QMutex 类用于同步访问。
transaction() 方法存储串行端口名称,超时和请求数据。 可以使用 QMutexLocker 锁定互斥锁以保护此数据。 线程可以启动,除非它已经在运行。 稍后将讨论 wakeOne() 方法。
在 run() 函数中,首先是锁定 QMutex 对象,然后使用成员数据获取串行端口名称,超时和请求数据。 完成此 *** 作后,将释放 QMutex 锁。
在任何情况下都不应在获取数据的过程中同时调用 transaction() 方法。 注意,虽然QString类是可重入的,但它不是线程安全的。 因此,建议不要在请求线程中读取串行端口名称,而在另一个线程中超时或请求数据。 MasterThread类一次只能处理一个请求。
在进入循环之前,将在 run() 方法中的堆栈上构造 QSerialPort 对象:
这样就可以在运行循环时创建对象。 这也意味着所有对象方法都在 run() 方法的范围内执行。
在循环内部检查当前事务的串行端口名称是否已更改。 如果已更改,则重新打开串行端口,然后重新配置。
循环将继续请求数据,写入串行端口并等待,直到所有数据都被传输为止。
警告:至于阻塞传输,应在每次write方法调用之后使用 waitForBytesWritten() 方法。 这将处理所有I / O例程,而不是Qt事件循环。
如果传输数据时发生超时错误,则发出 timeout() 信号。
成功请求后,有一个等待期的响应,然后再次读取。
警告:至于阻塞替代方法,应在每次 read() 调用之前使用 waitForReadyRead() 方法。 这将处理所有I / O例程,而不是Qt事件循环。
如果接收数据时发生超时错误,则发出timeout()信号。
成功完成事务后,response()信号包含从从应用程序接收的数据:
之后,线程进入睡眠状态,直到出现下一个事务。 线程在使用成员唤醒后读取新数据,并从头开始运行循环。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)