Qt 程序开始执行时,唯一的一个线程 —— 主线程 (main thread)也开始执行。主线程是唯一的,因为只有它才能创建 QApplication 或者是 QCoreApplication 对象,只有它才能通过应用程序对象调用 exec( ) 函数,只有它才能在 exec( ) 执行完毕后等待并处理事件。
主线程可以通过创建 QThread 子类对象开启一个新的线程,如果这些线程间需要相互通讯,它们可以使用共享变量,同时使用 mutexes,read-write locks,semaphores 或者 wait conditions 一些方法保持共享变量访问的同步性。但是由于这些技术可能锁定 event loop,同时还会冻结用户界面,所以其中没有一个能完成与主线程之间的通讯。
完成第二线程(secondary thread)与主线程之间的通讯的方法是:跨线程间的 signal-slot 连接。signal 一旦发出,其对应的 slot 函数便立即执行,这种连接是一种同步的机制。
但是当我们将不同线程中的对象连接在一起时,这种 signal-slot 通讯机制变得“不同步”(asynchronous)。signal-slot 机制的底层实现是传递一个 event,然后 slot 由 receiver 对象所在的线程中的 event loop 调用。默认情况下,一个 QObject 对象存在于创建它的线程中,但是任何时刻,调用 QObject : : moveToThread( ) 函数可以改变这种关系。
下面的示例用于演示如何进行跨线程间的 signal-slot 通讯。程序是一个基本的图像处理应用,可以对某个图像进行旋转、缩放等编辑:
这个程序中,使用了一个第二线程,专门处理图像,但是它并不锁定 event loop。这个第二线程有很多的任务需要完成,同时还需要向主窗口发送 events 来报告工作进度。
ImageWindow::ImageWindow()
{
imageLabel = new QLabel;
imageLabel->setBackgroundRole(QPalette::Dark);
imageLabel->setAutoFillBackground(true);
imageLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
setCentralWidget(imageLabel);
createActions();
createMenus();
statusBar()->showMessage(tr("Ready"), 2000);
connect(&thread, SIGNAL(transactionStarted(const QString &)),
statusBar(), SLOT(showMessage(const QString &)));
connect(&thread, SIGNAL(allTransactionsDone()),
this, SLOT(allTransactionsDone()));
setCurrentFile("");
}
在构造函数的部分,有一个 TransactionThread 对象,它是 QThread 的派生类,在这个应用程序中作为第二线程。在两个 connect( ) 中,都是这个线程对象向主线程(主窗口)发送信号。
程序中,进行图像处理的函数包括:水平翻转、垂直翻转、缩放图像、图像转换成 32 位,8位和 1 位等。这些函数都需要调用一个私有函数 addTransaction 将某个处理动作进行注册。例如下面的水平翻转函数:
void ImageWindow::flipHorizontally()
{
addTransaction(new FlipTransaction(Qt::Horizontal));
}
那么这个私有函数实现如下:
void ImageWindow::addTransaction(Transaction *transact)
{
thread.addTransaction(transact);
openAction->setEnabled(false);
saveAction->setEnabled(false);
saveAsAction->setEnabled(false);
}
这个函数将一个 transaction 添加到第二线程的 transaction 队列中,并在这些 transactions 正被处理时,将 Open,Save 和 Save As 这些菜单项禁用。
这里的 Transaction 类是一个抽象基类,它是所有处理图像的操作的抽象:
class Transaction
{
public:
virtual ~Transaction() { }
virtual QImage apply(const QImage &image) = 0;
virtual QString message() = 0;
};
其中虚析构函数是必须的,因为后面需要通过 Transaction 的指针删除一个子类的实例。Transaction 目前有三个具体的子类:FlipTransaction,ResizeTransaction 和 ConvertDepthTransaction 。这里只示例 FlipTransaction:
class FlipTransaction : public Transaction
{
public:
FlipTransaction(Qt::Orientation orientation);
QImage apply(const QImage &image);
QString message();
private:
Qt::Orientation orientation;
};
FlipTransaction 类主要实现基类中的两个纯虚函数,同时具有一个 Qt : : Orientation 类型的成员变量。它是一个枚举类型,用于指示对象的方位:Qt : : Horizontal 和 Qt : : Vertical 两个值。
apply( ) 函数的实现调用了 QIamge : : mirrored( ) 函数,返回的是翻转之后 QImage 对象,但是原图像对象不会改变:
QImage FlipTransaction::apply(const QImage &image)
{
return image.mirrored(orientation == Qt::Horizontal,
orientation == Qt::Vertical);
}
下面是 ResizeTransaction : : apply( ) 的实现:
returnimage.scaled(size,Qt::IgnoreAspectRatio,Qt::SmoothTransformation);
下面是 ConvertDepthTransaction : : apply( ) 的实现:
QImageConvertDepthTransaction::apply(constQImage&image)
{QImage::Formatformat;switch(depth){case1:format=QImage::Format_Mono;break;case8:format=QImage::Format_Indexed8;break;case24:default:format=QImage::Format_RGB32;}returnimage.convertToFormat(format);}
message( ) 函数返回一个 message 用于在状态栏中显示图像的处理情况。这个函数将在 transactionStarted( ) 信号发送时,在 Transaction : : run( ) 函数中调用。其它的 transactions 的子类都有类似的 message 函数。
接下来看,allTransactionDone( ) 这个 slot 函数。
void ImageWindow::allTransactionsDone()
{
openAction->setEnabled(true);
saveAction->setEnabled(true);
saveAsAction->setEnabled(true);
imageLabel->setPixmap(QPixmap::fromImage(thread.image()));
setWindowModified(true);
statusBar()->showMessage(tr("Ready"), 2000);
}
当 TransactionThread 的 transaction 队列中空了的时候,这个函数被调用。
下面是 TransactionThread 类:
class TransactionThread : public QThread
{
Q_OBJECT
public:
TransactionThread();
~TransactionThread();
void addTransaction(Transaction *transact);
void setImage(const QImage &image);
QImage image();
signals:
void transactionStarted(const QString &message);
void allTransactionsDone();
protected:
void run();
private:
QImage currentImage;
QQueue<Transaction *> transactions;
QWaitCondition transactionAdded;
QMutex mutex;
};
在这个类中,run 函数在自己的线程中执行,其它的函数则从主线程调用。在这个类中,维护着一个 transaction 队列,其中的每个 transaction 将一个接一个地被执行。成员变量分别为:currentImage 保存着一个当前将被处理的图像对象;trarnsactions 是 transactions 队列;transactionAdded 是一个 QWaitCondition 类对象,当新的 transaction 已加入队列时,这个条件用于唤醒线程;mutex 保护 currentImage 和 transactions 成员变量,以防止多个线程同时访问。
TransactionThread::TransactionThread()
{
start();
}
第二线程的构造函数中,调用 QThread : : start( ) 开启将要执行 transaction 的线程。在析构函数中,清空队列,将一个特殊的 EndTransaction 加入队列。唤醒线程,并使用 QThread : : wait( ) 等待线程结束。如果没有 wait( ),当其它的线程访问类中的成员变量时,程序有可能崩溃:
TransactionThread::~TransactionThread()
{
{
QMutexLocker locker(&mutex);
while (!transactions.isEmpty())
delete transactions.dequeue();
transactions.enqueue(EndTransaction);
transactionAdded.wakeOne();
}
wait();
}
在上面的代码中,大括号括起来的代码块中, QMutexLocker 的析构造函数将被调用。其中将 mutex 解锁,在 wait( ) 之前解锁这很重要,否则将引起死锁的可能性(第二线程一直等待 mutex 被解锁,而主线程一直等待第二线程完成而操持 mutex 不放)。
QWaitCondition : : wakeOne( ) 函数唤醒一个正在等待某个条件的线程。被唤醒的线程取决于操作系统的排程策略,并不能控制和提前预知哪个线程将被唤醒。如果需要唤醒某个指定的线程,通常需要使用不同的等待条件,使用不同的线程专门等待不同的等待条件。
addTransaction( ) 函数将一个 transaction 添加到队列中,并唤醒 transaction 线程。所有访问 transactions 的成员变量都由一个 mutex 保护,因为在第二线程遍历队列中的 transaction 时主线程可能修改这些变量。
void TransactionThread::addTransaction(Transaction *transact)
{
QMutexLocker locker(&mutex);
transactions.enqueue(transact);
transactionAdded.wakeOne();
}
第二线程,TransactionThread,的 run( ) 函数遍历其中的 transaction 队列,并执行每个 transaction 中的 apply( ) 函数,直到遇到 EndTransaction 这个 transaction 为止。如果队列为空,线程将等待“transaction added”条件。每次在执行一个 transaction 之前,总是先发送 transactionStarted( ) 信号以显示在主窗口的状态栏中,当所有的 transactions 都执行完毕后,发送一个 allTransactionsDone( ) 信号。
void TransactionThread::run()
{
Transaction *transact = 0;
QImage oldImage;
forever {
{
QMutexLocker locker(&mutex);
if (transactions.isEmpty())
transactionAdded.wait(&mutex);
transact = transactions.dequeue();
if (transact == EndTransaction)
break;
oldImage = currentImage;
}
emit transactionStarted(transact->message());
QImage newImage = transact->apply(oldImage);
delete transact;
{
QMutexLocker locker(&mutex);
currentImage = newImage;
if (transactions.isEmpty())
emit allTransactionsDone();
}
}
}