QT 开发基础知识(五)

原文:Foundations of Qt Development

协议:CC BY-NC-SA 4.0

十一、插件

Qt 提供了丰富的编程接口,能够与许多不同的技术进行交互。这种能力使得 Qt 驱动的应用程序在不同的平台上看起来不同;图像可以以多种不同的方式存储,并可以与多种数据库解决方案交互。你可能会惊讶地发现,你可以使用一个叫做插件的 Qt 特性来创建自己的新 Qt 特性。

Qt 用来处理插件的类并不局限于扩展 Qt。使用同一套类,你还可以创建自己的插件接口,并使用自定义插件扩展自己的应用程序。这使得创建可扩展的应用程序成为可能,而不必处理过程中涉及的所有平台细节。

插件基础知识

在开始使用插件之前,您需要了解插件是如何工作的。对于 Qt 应用程序来说,插件只是类的另一个实例。可用的方法由接口类决定。一个接口类通常只包含纯虚方法,所以接口类中没有实现任何函数。然后,插件继承了QObject类和接口类,并实现了所有具有特定功能的方法。当应用程序加载一个带有QPluginLoader类的潜在插件时,它会得到一个QObject指针。通过尝试使用qobject_cast将给定的对象转换为接口类,应用程序可以判断插件是否实现了预期的接口,是否可以被视为实际的插件。

为了让QPluginLoader正常工作,接口类必须通过使用Q_DECLARE_INTERFACE宏声明为接口,插件必须通过使用Q_INTERFACES宏声明它们实现了一个接口。这两个宏使您能够安全地将给定的插件匹配到正确的界面。这是 Qt 信任插件必须满足的一系列标准中的一步。下面的列表包含了 Qt 在试图加载一个插件时执行的所有检查。如果不满足任何标准,插件就不会被加载。

  • 必须使用相同版本的 Qt 来构建插件和应用程序。Qt 检查大调(4)和小调(4。 2 号匹配,但修订号(4.2。 2 可有所不同。
  • 插件和应用程序必须使用相同的编译器在相同的平台上为相同的操作系统构建。编译器的版本可以不同,只要它们的内部架构保持不变(例如,名称篡改)。
  • 用于插件和应用程序的 Qt 库必须以相同的方式配置,并且必须在“共享”模式下编译(不能使用带有静态 Qt 的插件)。

用插件扩展 Qt

Qt 有很多可以扩展的接口。例如,您可以为样式、数据库驱动程序、文本编解码器和图像格式添加插件。如果您使用 Qtopia Core,您甚至可以使用插件来访问不同的硬件,如图形驱动程序、鼠标驱动程序、键盘驱动程序和辅助设备。


Qtopia Core 是一个 Qt 版,用于掌上电脑、机顶盒、手机等嵌入式系统。


Qt 的可扩展性有很多好处。首先,它让 Qt 更加耐用,因为它可以适应新技术。还可以让 Qt 更轻便,因为不需要的插件不需要部署。这也确保了你可以继续使用 Qt 的应用编程接口,即使你需要针对特殊的技术。

创建 ASCII 艺术插件

制作 Qt 插件的原则是一样的,不管插件实际提供的扩展类型是什么。为了理解如何扩展 Qt 以及 Qt、插件和应用程序之间的交互是如何工作的,我们来看看一个图像格式插件。该插件会将图像保存为 ASCII art,其中每个像素被转换为一个字符(图 11-1 中的显示了一个例子)。这是一种失传的艺术,但在 20 世纪 80 年代和 90 年代初非常普遍。

在你开始看这个插件之前,你应该看看 Qt 是如何加载和保存图片的。总体思路是使用来自QImage类的saveload方法。(你可以在QImage的构造器中指定文件名,而不是使用load——它做同样的事情。)

QImage类在加载图像时使用了一个QImageReader类。QImageReader检查是否有能够读取给定图像的QImageIOPlugin。当一个插件被找到时,它被要求返回一个QImageIOHandler,然后QImageReader用它来实际读取图像。

写的时候过程差不多,但是文件格式不是从文件中确定的而是要在调用save的时候指定。QImage将它传递给QImageWriter类,该类询问是否有能够以给定格式保存的QImageIOPlugin。当找到后,QImageIOPlugin返回一个QImageIOHandlerQImageWriter用它将图像写入一个设备,通常是一个文件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-1。 一幅 ASCII 艺术图像


提示图像读取器和写入器与QIODevice对象一起工作,因此图像可以被读取或写入网络流、内存缓冲区、文件——你能想到的——因为QIODevice是管理这些接口的类的基类。


读写两种情况如图图 11-2 所示。图中还显示了哪个部分是 Qt,哪个部分是插件。所示场景通常与 Qt 插件一起使用。一个是询问插件必须提供什么,然后返回执行实际任务的实例。在图像插件的情况下,查询QImageIOPlugin并返回一个QImageIOHandler

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-2。 使用 Qt 读写图像步骤中涉及的类

插件

现在你已经准备好看看可以处理文本图像的 ASCII 艺术图像插件;格式叫做ti。您还将告诉 Qt 使用ti作为这些文本图像的首选文件扩展名。TextImagePlugin类继承了QImageIOPlugin类,而TextImageHandler继承了QImageIOHandler类(插件中没有其他东西)。

让我们开始看代码,从清单 11-1 中TextImagePlugin的类声明开始。该接口由三种方法组成:keyscapabilitiescreatekeys方法返回插件支持的图像格式的QStringListcapabilities方法将一个QIODevice和一个图像格式作为参数,然后返回一个值,指示插件CanReadCanWrite是否将指定的格式发送到给定的设备或从给定的设备接收指定的格式。最后一个方法create,为给定的设备和格式创建一个QImageIOHandler


注意如果支持增量读取,capabilities方法可以返回值CanReadIncremental。这意味着它可以多次读取图像,从而逐渐显示图像。ASCII 艺术图像插件从不试图实现它。


**清单 11-1。**镜像 IO 插件的类声明

class TextImagePlugin : public QImageIOPlugin

{

public:

  TextImagePlugin();

  ~TextImagePlugin();

  QStringList keys() const;

  Capabilities capabilities( QIODevice *device, const QByteArray &format ) const;

  QImageIOHandler *create( QIODevice *device,

    const QByteArray &format = QByteArray() ) const;

};

最有趣的方法是capabilities(如清单 11-2 中的所示),它决定了插件可以为给定格式的设备做什么。这意味着formatQByteArray必须包含字符串ti或者为空,插件才能对它做任何事情。

如果格式QByteArray为空,则必须查看QIODevice。如果它是开放且可写的,您可以随时写入。如果它是可读的,并且插件可以从中读取(稍后将详细介绍静态的canRead方法),你就可以从中读取。重要的是不要以任何方式影响设备(确保您只是在偷看;实际上不读、不写、不找)。


A QByteArray可以看作是 Qt 对char*的控制版本。你可以用它来携带文本,就像普通的 C 字符串一样。千万不要用QString来做这件事(就像你可能用std::string做过的那样),因为它会在内部转换成 Unicode,这可能会破坏你的二进制数据。


清单 11-2。 决定插件可以对给定的格式和设备做什么

QImageIOPlugin::Capabilities TextImagePlugin::capabilities( QIODevice *device,

  const QByteArray &format ) const

{

  if( format == "ti" )

    return (QImageIOPlugin::CanRead | QImageIOPlugin::CanWrite);

  if( !format.isEmpty() )

    return 0;

  if( !device->isOpen() )

    return 0;

  QImageIOPlugin::Capabilities result;

  if( device->isReadable() && TextImageHandler::canRead( device ) )

    result |= QImageIOPlugin::CanRead;

  if( device->isWritable() )

    result |= QImageIOPlugin::CanWrite;

  return result;

}

那么 Qt 怎么知道要哪些格式呢?所有的图像插件都报告它们可以用keys方法处理哪些格式。格式(在本例中是 format)被放入一个返回的QStringList中。实现如清单 11-3 所示。

清单 11-3。 将图像文件格式放入 QStringList

QStringList TextImagePlugin::keys() const

{

  return QStringList() << "ti";

}

当格式正确并且可以处理时,最后一个方法开始起作用。清单 11-4 中的create方法创建了一个自定义TextImageIOHandler的实例,用格式和设备对其进行配置,并返回结果。

为处理程序设置了一种格式,因此它可以处理几种格式。有许多格式几乎是相同的,因此减少源代码的大小是很有用的。

清单 11-4。 创建和配置镜像 IO 处理器

QImageIOHandler *TextImagePlugin::create( QIODevice *device,

  const QByteArray &format ) const

{

  QImageIOHandler *result = new TextImageHandler();

  result->setDevice( device );

  result->setFormat( format );

  return result;

}

在进入 handler 类之前,必须告诉 Qt 这个类是插件接口的一部分。你可以通过使用Q_EXPORT_PLUGIN2宏来做到这一点,如清单 11-5 所示。宏放在实现文件中的某个地方(不是头文件)。第一个参数是所有字符都是小写的类名,而第二个参数是实际的类名。

宏告诉 Qt 这个类是插件的接口。每个插件只能有一个接口,所以这个宏在每个插件中只能使用一次。

清单 11-5。 将类导出为插件

Q_EXPORT_PLUGIN2( textimageplugin, TextImagePlugin )

读取和写入图像

TextImagePlugin占了插件的一半。另一半由TextImageHandler类组成,该类执行所有繁重的工作——从设备读取和写入图像。

让我们先看看清单 11-6 中的类声明。该类继承了QImageIOHandler类并实现了方法readwritecanRead的两个变体。readwrite方法非常简单明了,但是两个canRead版本需要一点解释。非静态版本简单地调用静态版本。拥有静态版本的原因是从TextImagePlugin类中的capabilities方法更容易使用(参考清单 11-2 )。从 Qt 的角度来说,不需要静态版本。

清单 11-6。 图像 IO 处理程序的类声明

class TextImageHandler : public QImageIOHandler

{

public:

  TextImageHandler();

  ~TextImageHandler();

  bool read( QImage *image );

  bool write( const QImage &image );

  bool canRead() const;

  static bool canRead( QIODevice *device );

};

最简单的复杂方法是write方法,如清单 11-7 所示。它只需要很少的错误检查,只是将部分图像流式传输到一个写入指定设备的QTextStreamdevice方法返回与在TextImagePlugincreate方法中使用setDevice设置的设备相同的设备(参见清单 11-4 )。它在创建文本流stream时使用。

建立流时,会向文件中写入一个前缀。所有 ASCII 艺术图像都以一行TEXT开始。然后将尺寸写成 × ,其中x作为分隔符。您可以从作为方法参数给出的图像中获取尺寸。前缀和维度构成了标题;剩下的就是图像数据了。

通过将每个像素的红色、绿色和蓝色值转换成平均灰度值来计算图像数据。然后,该值向下移位并屏蔽为三位,取值范围为 0–7。该值对应于每个像素的暗度,用于在map字符串中查找字符。

map变量是一个初始化为.:ilNAMchar*(包括一个初始空格)。map字符串中的字符已被挑选,因此最低值为白色,并且随着索引的增加,每个字符变得越来越暗。在图 11-3 中可以看到源图像和产生的 ASCII 艺术。ASCII 艺术在文字处理器中使用设置为很小尺寸的等宽字体显示。

当所有图像数据写入流时,在返回true进行成功的写操作之前,确保流的良好状态。

清单 11-7。 将图像写入设备

bool TextImageHandler::write( const QImage &image )

{

  QTextStream stream( device() );

  stream << "TEXT\n";

  stream << image.width() << "x" << image.height() << "\n";

  for( int y=0; y<image.height(); ++y )

  {

    for( int x=0; x<image.width(); ++x )

    {

      QRgb rgb = image.pixel( x, y );

      int r = rgb & 0xff;

      int g = (rgb >> 8) & 0xff;

      int b = (rgb >> 16) & 0xff;

      stream << map[ 7 - (((r+g+b)/3)>>5) & 0x7 ];

    }

    stream << "\n";

  }

  if( stream.status() != QTextStream::Ok )

    return false;

  return true;

}

今天的大多数字体不是等宽的,这意味着一个字符的宽度取决于字符;一辆 i 比一辆 M 需要更少的空间。另一个问题是,大多数字体的高度都比宽度大。ASCII 艺术图像插件没有考虑到这一点,所以即使使用等宽字体,结果看起来也会被拉伸。在write方法中很难对此进行补偿,因为你永远不知道用户将使用哪种字体来查看图像。总而言之,结果并不完美,但你仍然可以看出图像显示了什么。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-3。 源图像旁边是生成的 ASCII 图片

虽然写是一个简单的过程,但是读是完全相反的,因为你永远不能相信输入流是有效的。它可以包含任何内容,包括完全意想不到的内容(例如,损坏的数据或完全不同的文件格式),或者文件可能缺少数据。这意味着read方法比write方法更复杂。

在清单 11-8 中,你可以看到标题是如何被读取和验证的。与编写一样,它从创建一个QTextStream开始。读取第一行,并确保它等于TEXT。如果没有,则整个操作中止。

第一行后面的维度被匹配,并使用正则表达式过滤掉。如果表达式不匹配,或者任何维度无法转换为数字,操作将中止。现在你知道标题是好的,所以你可以开始读取图像数据。

清单 11-8。 确定你是否愿意阅读文件

bool TextImageHandler::read( QImage *image )

{

  QTextStream stream( device() );

  QString line;

  line = stream.readLine();

  if( line != "TEXT" || stream.status() != QTextStream::Ok )

    return false;

  line = stream.readLine();

  QRegExp re( "(\\d+)x(\\d+)" );

  int width, height;

  if( re.exactMatch( line ) )

  {

    bool ok;

    width = re.cap(1).toInt( &ok );

    if( !ok )

      return false;

    height = re.cap(2).toInt( &ok );

    if( !ok )

      return false;

  }

  else

    return false;

...

}

因为头是有效的,所以可以看到read方法的后半部分(源代码如清单 11-9 所示)。阅读和写作非常相似。首先,创建一个临时的QImage;然后读取每一行并转换成灰度。根据预期的图像宽度检查每行的长度,图像数据中不接受任何意外字符。如果整个图像读取完毕后流的状态正常,则在返回true以指示读取成功之前,作为参数给出的图像会被更新。

清单 11-9。 从设备中读取图像,判断是否一切顺利。

bool TextImageHandler::read( QImage *image )

{

...

  QImage result( width, height, QImage::Format_ARGB32 );

  for( int y=0; y<height; ++y )

  {

    line = stream.readLine();

    if( line.length() != width )

      return false;

    for( int x=0; x<width; ++x )

    {

      switch( QString(map).indexOf(line[x]) )

      {

        case 0:

          result.setPixel( x, y, 0xffffffff );

          break;

        case 1:

          result.setPixel( x, y, 0xffdfdfdf );

          break;

        case 2:

          result.setPixel( x, y, 0xffbfbfbf );

          break;

        case 3:

          result.setPixel( x, y, 0xff9f9f9f );

          break;

        case 4:

          result.setPixel( x, y, 0xff7f7f7f );

          break;

        case 5:

          result.setPixel( x, y, 0xff5f5f5f );

          break;

        case 6:

          result.setPixel( x, y, 0xff3f3f3f );

          break;

        case 7:

          result.setPixel( x, y, 0xff000000 );

          break;

        default:

          return false;

      }

    }

  }

  if( stream.status() != QTextStream::Ok )

    return false;

  *image = result;

  return true;

}

将图像保存为 ASCII 图片,然后读取会导致一些损失。颜色到灰度的转换和反向转换远非完美。从图 11-3 的中取出 ASCII 艺术图像,并保存回一个普通的基于像素的图像,得到如图 11-4 的所示的图像。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-4。 将 ASCII 图片另存为普通图片。

TextImageHandler的剩余部分是清单 11-10 中显示的canRead方法。非静态方法调用静态方法。非静态方法实际上只是一个提供 Qt 期望的接口的包装器。静态方法使用peek方法来查看文件是否以前缀TEXT开头。如果找到了前缀,就认为文件的其余部分是正常的,并返回true以指示处理程序可以读取该文件。


提示在设计文件格式时,给实际数据加上一个唯一的头是个好主意。这使得无需读取整个文件就可以查看该文件是否适合读取。


这里使用peek方法很重要,因为它不会影响QIODevice。当试图读取图像时,Qt 可以将同一个设备传递给几个插件,以确定使用哪个插件。

清单 11-10。 窥视设备以确定图像看起来是否正确。

bool TextImageHandler::canRead( QIODevice *device )

{

  if( device->peek(4) == "TEXT" )

    return true;

  return false;

}

bool TextImageHandler::canRead() const

{

  return TextImageHandler::canRead( device() );

}

建造和安装

要构建并安装一个插件,让 Qt 能够找到它,不仅仅需要运行qmake –project。您可以使用它来创建一个起点,但是您必须广泛地修改项目文件。

清单 11-11 显示了 ASCII 艺术图像格式插件的项目文件。HEADERSSOURCES行对于所有 Qt 项目都是一样的。上面的行表示您正在构建一个模板,而下面的行表示插件将被安装的位置。

从顶部开始,您将TEMPLATE设置为lib,这告诉 QMake 您正在构建一个库,而不是一个应用程序。下一行告诉 QMake 插件的名称:textimage。下面是CONFIG行,其中您指定了lib将被用作plugin,并且它应该以release模式构建(没有调试信息)。顶部的最后一行是VERSION行,用于区分不同的插件版本。在这种情况下,结果文件被命名为textimage1

最后两行设置了一个安装目标,它配置了运行make install时执行的操作。这一段的第一行将targetpath设置为$$[QT_INSTALL_PLUGINS]/imageformats——也就是 Qt 安装目录里面的plugins/imageformats目录。本节的第二行和项目文件的最后一行告诉 Qt 在make install运行时安装target。它会将插件文件复制到适当的目录,让 Qt 能够找到它。

清单 11-11。 项目文件为 TextImagePlugin TextImageHandler

TEMPLATE = lib

TARGET = textimage

CONFIG += plugin release

VERSION = 1.0.0

HEADERS += textimagehandler.h textimageplugin.h

SOURCES += textimagehandler.cpp textimageplugin.cpp

target.path += $$[QT_INSTALL_PLUGINS]/imageformats

INSTALLS += target

要构建和制作这个项目,必须运行qmake,然后运行make。如果它没有任何问题地完成了,你可以运行make install让这个插件对 Qt 可用。

使用插件

在你开始使用插件之前,你需要知道 Qt 是如何处理插件的。它们是由QApplication(实际上是由它的超类— QCoreApplication)对象加载的,所以当你使用插件时,你必须确保有一个QApplication的实例可用。

有了一个QApplication对象后,可以通过使用静态的supportedImageFormats方法查询QImageReaderQImageWriter类,以获得支持的格式列表。读取器返回可读的图像格式,而写入器返回可写的图像格式。返回值是QByteArray对象的QList,它是从不同的QImageIOPlugin对象返回的所有可用键的列表。

清单 11-12 显示了一个小的foreach循环,它查询所有可读的图像格式并将它们打印到调试控制台。所有可以读取的格式通常也可以被编写——但是你永远不能假设这一点。

清单 11-12。 向 Qt 询问可以读取的图像格式

   QApplication app( argc, argv );

   foreach( QByteArray ba, QImageReader::supportedImageFormats () )

     qDebug() << ba;

在读取时,Qt 通常通过查询插件的capabilities方法来确定文件格式。这会生成对不同的canRead方法的调用,这些方法决定特定的插件是否可以处理给定的文件。(应用程序只需要指定文件名;Qt 完成剩下的工作。如清单 11-13 所示,如果加载失败,产生的QImage是一个空图像。如果使用QImageload方法,可以从中获得返回值。如果加载了图像,该方法返回true;如果失败,它将返回false

清单 11-13。 阅读一幅 ASCII 艺术图像

   QImage input( "input.ti" );

   if( input.isNull() )

     qDebug() << "Failed to load.";

阅读的对立面——储蓄——稍微复杂一些。因为没有要查找的文件前缀,所以需要在调用save时指定文件格式(见清单 11-14 )。在清单中,从磁盘中读取了一个png映像。如果读取成功,图像将再次保存为ti图像。save调用返回一个bool值,该值指示操作是否成功。值true意味着它起作用了。

清单 11-14。 写一个 ASCII 艺术图像

   QImage input( "input.png" );

   if( input.isNull() )

     qDebug() << "Failed to load.";

   else

     if( !input.save( "test.ti", "ti" ) )

       qDebug() << "Failed to save.";

使用插件扩展你的应用程序

扩展 Qt 是一回事,但是让你自己的应用程序可扩展是另一回事。它不仅涉及实现给定的接口;你还必须设计界面,寻找插件,加载它们,然后使用它们。

这是传统上需要考虑很多很多平台问题的领域之一。有了 Qt,几乎所有这些怪癖都消失了,您可以专注于为您的用户提供模块化和可扩展的设计。

过滤图像

本章从 Qt 的图像文件格式插件开始;它继续创建一个图像过滤应用程序,其中的过滤器是作为插件提供的。在图 11-5 中可以看到一目了然的情况:过滤器位于左侧和右侧;原始图像出现在过滤后的图像上方。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-5。 图像过滤应用在行动中

界面

过滤器用于获取一个图像并返回一个新图像,该新图像是给定图像的转换版本,这意味着它需要一个获取图像并返回图像的方法。因为您计划将它作为插件加载,所以应用程序无法从一开始就知道每个过滤器的名称——因此它还需要一个返回其名称的方法。

你如何将这些代码转换成一个真正的插件界面?一个 Qt 插件接口被定义为一个由纯虚拟方法组成的类。这意味着作为插件一部分的所有方法都被设为virtual并且没有被实现。相反,它们在类声明中被标记为=0

结合插件接口和过滤器插件需要做什么的知识,你得到了类似于清单 11-15 中所示的FilterInterface类。name方法返回过滤器的名称,filter方法过滤给定的QImage并返回过滤结果。名字很清楚,并且很容易理解事情应该如何工作。

清单 11-15。ImageFilter接口类

class FilterInterface

{

public:

  virtual QString name() const = 0;

  virtual QImage filter( const QImage &image ) const = 0;

};

在这个类可以作为插件接口使用之前,你必须通过使用清单 11-16 中显示的行告诉 Qt 它是一个接口。第一个参数是涉及的类;第二个是标识符字符串,它对于接口必须是唯一的。

清单 11-16。 声明 ImageFilter 为 Qt 的插件接口

Q_DECLARE_INTERFACE( FilterInterface,

  "se.thelins.CustomPlugin.FilterInterface/0.1" )

当界面被定义后,开发可以分成两部分:插件和应用程序(界面的两边)。

实施过滤器

让我们先来看看图 11-5 中的过滤插件。这个类被称为Flip(它的声明如清单 11-17 中的所示)。头文件包括过滤器接口类声明,因此插件知道如何根据接口规范定义类。

如清单所示,Flip继承了QObjectFilterInterface。重要的是QObject先遗传;否则元对象编译器将失败。然后,类声明以Q_OBJECT宏开始,后跟一个Q_INTERFACES宏,表明该类实现了FilterInterface接口。

遵循宏声明,您将找到所需的方法。因为基类只包含纯虚方法,所以所有方法都必须在这里实现。否则,插件类不能被实例化。

清单 11-17。 过滤器的类声明 Flip

#include "filterinterface.h"

class Flip : public QObject, FilterInterface

{

  Q_OBJECT

  Q_INTERFACES(FilterInterface)

public:

  QString name() const;

  QImage filter( const QImage &image ) const;

};

name方法的实现非常简单。因为名称在用户界面中使用,所以它以比仅仅Flip更容易阅读的形式传递。源代码可以在清单 11-18 中看到。

清单 11-18。Flip的全称是 "Flip Horizontally"

QString Flip::name() const

{

  return "Flip Horizontally";

}

filter方法稍微复杂一些(参见清单 11-19 中的实现源代码)。根据给定输入图像的尺寸和格式创建结果图像。则在返回结果图像之前进行翻转。

清单 11-19。filter方法翻转给定的图像并返回结果。

QImage Flip::filter( const QImage &image ) const

{

  QImage result( image.width(), image.height(), image.format() );

  for( int y=0; y<image.height(); ++y )

    for( int x=0; x<image.width(); ++x )

      result.setPixel( x, image.height()-1-y, image.pixel( x, y ) );

  return result;

}

在你完成Flip过滤器的实现之前,你必须告诉 Qt 这个类实现了插件的接口。这是通过使用Q_EXPORT_PLUGIN2来完成的,就像使用图像文件格式插件一样(见清单 11-20 )。

清单 11-20。 一定要告诉 QtFlip是插件接口。

Q_EXPORT_PLUGIN2( flip, Flip )

构建Flip插件非常类似于构建图像文件格式插件。在清单 11-21 所示的项目文件中,模板被设置为lib,依此类推。过滤器放在应用程序目录的子目录filters/flip中,因此filterinterface.h文件需要在INCLUDEPATH中。这意味着将其设置为../..以包含该搜索路径。安装路径是../../plugins,因此相应地设置目标的路径。

清单 11-21。 项目文件用于构建 Flip 插件

TEMPLATE = lib

TARGET = flip

CONFIG += plugin release

VERSION = 1.0.0

INCLUDEPATH += ../..

HEADERS += flip.h

SOURCES += flip.cpp

target.path += ../../plugins

INSTALLS += target

图 11-5 显示了Flip滤镜旁边的BlurDarken滤镜。这些过滤器也被实现为插件。除了返回的名称和实际的过滤算法之外,实现非常相似。

应用程序

FilterInterface类的另一边是使用过滤器插件的应用程序。这个应用程序很简单:它由一个使用 Designer 构建的对话框、该对话框的一个实现和一个显示该对话框的简单的main函数组成。

对话框设计由一个QListWidget和两个QLabel小部件组成。根据设计者的对话框结构如图图 11-6 所示。该对话框由水平布局组成,因此列表显示在标签的左侧。(参见图 11-5 查看运行中的对话框。)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-6。 对象检查器显示了 FilterDialog的结构。

在您开始详细研究FilterDialog类之前,您必须熟悉您将在应用程序中使用的策略。在 Qt 中使用插件时,使用 QPluginLoader类来加载插件并创建实现插件接口的对象的实例。您找到的实例放在一个QMap中,它将过滤器名称映射到实际的过滤器对象。然后,当用户请求应用过滤器时,该映射用于访问过滤器。

现在您已经准备好开始查看源代码了。清单 11-22 显示了FilterDialog类的类声明,它实现了保存在ui成员变量中的设计器对话框。成员变量filters用于保存加载的过滤器插件。

当用户选择一个过滤器时,槽filterChanged被调用。从构造器调用的findFilters方法查找加载并列出插件。

清单 11-22。FilterDialog类声明

class FilterDialog : public QDialog

{

  Q_OBJECT

public:

  FilterDialog( QWidget *parent=0 );

private slots:

  void filterChanged( QString );

private:

  void findFilters();

  QMap<QString, FilterInterface*> filters;

  Ui::FilterDialog ui;

};

在清单 11-23 中显示的构造器使用uic从设计器文件中生成的setupUi方法初始化用户界面。然后它设置一个原始图像并将QListWidget currentTextChanged信号连接到filterChanged插槽。

当用户界面被设置和配置后,在显式调用一次filterChanged槽以生成结果图像之前,调用findFilters方法。

清单 11-23。 构造器为 FilterDialog

FilterDialog::FilterDialog( QWidget *parent ) : QDialog( parent )

{

  ui.setupUi( this );

  ui.originalLabel->setPixmap( QPixmap( "source.jpeg" ) );

  connect( ui.filterList, SIGNAL(currentTextChanged(QString)),

           this, SLOT(filterChanged(QString)) );

  findFilters();

  filterChanged( QString() );

}

大多数有趣的事情都发生在findFilters方法中。该方法的源代码可在清单 11-24 中找到。

从清单中可以看出,QPluginLoader本身并不定位插件。相反,您使用一个QDir对象来查找您期望插件所在的目录中的所有文件。前两行高亮显示的代码为每个找到的文件创建一个QPluginLoader对象,并尝试创建一个插件类的实例。

如果返回的实例不为空,您尝试使用qobject_cast方法将其转换为FilterInterface类(这显示在最后一行突出显示的内容中)。如果FilterInterface指针不为空,那么您已经找到了一个实际的过滤器,因此您可以将该过滤器添加到filters地图中,并在QListWidget中显示其名称。

如果任何突出显示的步骤导致空值,表明文件无法加载,这可能是由于几个原因:文件不包含插件,插件是使用错误的工具或错误的 Qt 版本构建的,或者插件没有实现FilterInterface接口。在任何情况下,插件都是无效的,应用程序也不感兴趣。

清单 11-24。 找到插件,加载插件,放入列表

void FilterDialog::findFilters()

{

  QDir path( "./plugins" );

  foreach( QString filename, path.entryList(QDir::Files) )

  {

    QPluginLoader loader( path.absoluteFilePath( filename ) );

    QObject *couldBeFilter = loader.instance();

    if( couldBeFilter )

    {

      FilterInterface *filter = qobject_cast<FilterInterface*>( couldBeFilter );

      if( filter )

      {

        filters[ filter->name() ] = filter;

        ui.filterList->addItem( filter->name() );

      }

    }

  }

}

当用户从过滤器列表中选择一个插件时,filterChanged插槽被调用(该插槽如清单 11-25 中的所示)。如果滤镜为空,原始图像显示在filteredLabel标签中;否则你可以使用filters地图来找到所选的过滤器。滤镜应用于来自originalLabel标签的图像,生成的QImage被分配给filteredLabel标签。

清单 11-25。 当用户从列表中选择一个时应用过滤器

void FilterDialog::filterChanged( QString filter )

{

  if( filter.isEmpty() )

  {

    ui.filteredLabel->setPixmap( *(ui.originalLabel->pixmap() ) );

  }

  else

  {

    QImage filtered = filters[ filter ]->

      filter( ui.originalLabel->pixmap()->toImage() );

    ui.filteredLabel->setPixmap( QPixmap::fromImage( filtered ) );

  }

}

拼图的最后一块是一个main函数,它创建一个QApplication对象,然后显示对话框。项目文件不受插件使用的影响,所以运行qmake -project,然后运行qmakemake,就可以完成这项工作。


注意因为滤镜的源文件位于包含应用程序的目录下的子目录中,qmake -project命令会将滤镜的源文件和应用程序的文件一起包含在项目中。在构建或添加一个-norecursive开关到qmake调用以阻止qmake窥视子目录之前,确保从结果项目文件中删除过滤器的文件。


所有这些代码将把你带到图 11-5 所示的应用程序。回头看看代码的大小,很难看出这个应用程序有多强大。几乎可以无限制的扩展和修改,增加的复杂度相对较小。

合并插件和应用程序

您可能希望有插件,但也希望在应用程序可执行文件中保留一些功能(例如,出于部署原因)。发布一个可执行文件总是比发布一个可执行文件和一堆插件容易。也许一些插件是应用程序有用所必需的;例如,一个开发环境至少需要一个代码编辑器才能工作。那么将该编辑器包含在实际的应用程序可执行文件中是合乎逻辑的,即使它在内部被视为一个插件。

Qt 使您能够以一种简单的方式做到这一点,包含的插件可以使用QPluginLoader来定位,并因此被添加到用于其余插件的同一个流中(它确实涉及到插件项目和应用程序本身的变化)。

制作静态插件

当你构建一个插件时,你构建了一个动态链接库(DLL) 。如果你在你的项目文件中添加一行CONFIG += static,产生的库将用于静态链接。这意味着库是在链接时添加到应用程序中的,而不是在运行时动态加载的。

当适应静态链接时,Darken插件的项目文件如清单 11-26 所示。将它与来自清单 11-21 的Flip插件的项目文件进行比较。

清单 11-26。 项目文件为静态链接插件

TEMPLATE = lib

TARGET = darken

CONFIG += plugin release

VERSION = 1.0.0

INCLUDEPATH += ../..

HEADERS += darken.h

SOURCES += darken.cpp

target.path += ../../plugins

INSTALLS += target

CONFIG += static

链接和查找插件

对应用程序的更改可以分为三个部分。首先,您必须将库添加到项目文件中,以便在构建可执行文件时将它链接到应用程序。清单 11-27 显示了应用程序的项目文件。

突出显示的行使用添加库搜索路径的–L命令行选项和添加库引用的–l选项添加对静态链接库的引用。添加的搜索路径取决于用于构建库的平台。

清单 11-27。 引用静态链接插件的应用项目文件

TEMPLATE = app

TARGET =

DEPENDPATH += .

INCLUDEPATH += .

# Input

HEADERS += filterdialog.h filterinterface.h

FORMS += filterdialog.ui

SOURCES += filterdialog.cpp main.cpp

win32:LIBS += -L./filters/darken/release/ -ldarken

!win32:LIBS += -L./filters/darken -ldarken

其次,通过添加清单 11-28 中所示的代码行,确保QPluginLoader仍然可以找到插件,即使它静态链接到应用程序。

注意,宏Q_IMPORT_PLUGIN期望的是小写字符的类名,而不是实际的类名。这是作为插件源代码中的Q_EXPORT_PLUGIN2宏的第一个参数给出的字符串。

清单 11-28。QPluginLoader通知静态链接 Darken 插件的存在。

*`Q_IMPORT_PLUGIN( darken )

int main( int argc, char **argv )
{

}`

对应用程序的第三个也是最后一个更改是在FilterDialog类的findFilters方法中。清单 11-29 中显示了该方法的更新版本。突出显示的行显示了对QPluginLoader::staticInstances方法的调用,该方法返回指向所有静态链接插件的QObject指针。然后可以使用qobject_cast将指针转换为FilterInterface指针;如果转换操作不返回 null,则表示找到了筛选器。

与动态加载插件相比,查找文件并加载的步骤已经被staticInstances调用所取代。这是一个明显的变化,因为插件包含在应用程序的可执行文件中,所以不需要寻找或加载外部文件。

清单 11-29。 查询 QPluginLoader 进行静态链接过滤

void FilterDialog::findFilters()

{

  foreach( QObject *couldBeFilter, QPluginLoader::staticInstances() )

  {

    FilterInterface *filter = qobject_cast<FilterInterface*>( couldBeFilter );

    if( filter )

    {

      filters[ filter->name() ] = filter;

      ui.filterList->addItem( filter->name() );

    }

  }

  QDir path( "./plugins" );

  foreach( QString filename, path.entryList(QDir::Files) )

  {

    QPluginLoader loader( path.absoluteFilePath( filename ) );

    QObject *couldBeFilter = loader.instance();

    if( couldBeFilter )

    {

      FilterInterface *filter = qobject_cast<FilterInterface*>( couldBeFilter );

      if( filter )

      {

        filters[ filter->name() ] = filter;

        ui.filterList->addItem( filter->name() );

      }

    }

  }

}

对应用程序的更改不会改变用户的体验。在前面的例子中,唯一的区别是Darken过滤器总是可用的,即使没有插件可以加载。

注意,实际使用过滤器的方法也没有发生变化。filterChange方法不关心插件是如何被链接的。

一个工厂接口

比较图像过滤器的插件接口和图像文件格式的接口,有一个很小但很重要的区别:过滤器插件每个插件只能包含一个过滤器,而由于插件接口的设计方式,一个插件中可以有多种文件格式。文件格式插件可以被认为是一个文件格式工厂,所以插件为应用程序提供文件格式,而不是直接处理它们。

让插件充当工厂可能非常有用,因为使用工厂创建的实际工作类可以共享代码并相互继承。您还可以通过将插件组合成几个大插件来简化部署,而不必处理大量的小插件。通过使用智能工厂接口,甚至可以将几个不同类型的插件组合在一个插件中。

不要把FilterInterface分成FilterPluginInterfaceFilterWorker,你可以很容易地扩展FilterInterface来通过一个接口处理多个过滤操作。这样做需要改变界面本身,这意味着改变所有的插件以及应用程序本身。

新界面

对接口的更改使得每个FilterInterface可以返回几个名称,并且可以在调用filter方法时指定过滤器。新FilterInterface的源代码如清单 11-30 所示(与清单 11-15 和清单 11-16 所示的原始界面进行比较)。

name方法已经被重命名为names,并返回一个QStringList而不是一个QStringfilter方法被赋予了一个新的参数,指定了要使用的过滤器的名称。最后,传递给Q_DECLARE_INTERFACE宏的标识符字符串中的版本号已经更新,表明接口已经改变,旧插件不兼容。

清单 11-30。 新的 FilterInterface 可以通过一个界面处理多个滤镜。

class FilterInterface

{

public:

  virtual QStringList names() const = 0;

  virtual QImage filter( const QString &filter, const QImage &image ) const = 0;

};

Q_DECLARE_INTERFACE( FilterInterface,

  "se.thelins.CustomPlugin.FilterInterface/0.2" )

确定是应用程序还是插件负责确保没有无效的过滤器名称作为参数传递给filter方法是很重要的。如果发生这种情况,插件必须做好准备(不要让整个应用程序崩溃)。

更新插件

将旧插件转换成新界面很容易。在从names返回之前,只需将名称放入QStringList中,然后忽略filter方法中的过滤器名称参数。扩展一个旧插件几乎一样容易。从names方法中返回几个名称,并通过使用过滤器名称参数来确定在filter方法中使用哪个过滤器。

清单 11-17 到清单 11-21 中的Flip滤镜已经扩展到支持水平和垂直翻转。

在清单 11-31 中显示的names方法中做了小的改变。它现在返回两个QString,每个过滤器一个。

清单 11-31。 返回几个名字使用一个 QStringList

QStringList Flip::names() const

{

  return QStringList() << "Flip Horizontally" << "Flip Vertically";

}

filter方法如清单 11-32 所示。突出显示的行显示了在哪里对filter参数进行评估,以确定要做什么。

注意,如果给定了一个意外的过滤器名称,过滤器将执行一个垂直翻转。尽管这可能不是用户所期望的,但它将使应用程序保持运行——所以这是一个处理它的好方法,因为这个问题没有特定的解决方案。也许可以返回一个无效的QImage,但是整个讨论都是关于一个应用程序 bug 将如何出现(所以不值得在这个问题上浪费太多精力)。更好的保证应用中没有这样的 bug!

清单 11-32。 根据 filter 参数的不同,过滤器的行为也不同。

QImage Flip::filter( const QString &filter, const QImage &image ) const

{

  bool horizontally = (filter=="Flip Horizontally");

  QImage result( image.width(), image.height(), image.format() );

  for( int y=0; y<image.height(); ++y )

    for( int x=0; x<image.width(); ++x )

      result.setPixel(

        horizontally?x:(image.width()-1-x),

        horizontally?(image.height()-1-y):y,

        image.pixel( x, y ) );

  return result;

}

项目不会受到变化的影响,所以重新编译和安装产生的插件是启动和运行所必需的。

更换装载器

在应用程序端,QPluginLoader仍然与QDir结合使用,从FilterDialog中的findFilters方法中找到并加载插件。然而,对于找到的每个滤波器,可以将几个滤波器添加到QListWidgetfiltersQMap。新的findFilters方法如清单 11-33 所示。高亮显示的行显示返回的名称被逐个添加到地图和列表小部件中。将此清单与清单 11-29 进行比较。

清单 11-33。findFilters方法从每个插件中添加几个滤镜。

void FilterDialog::findFilters()

{

  foreach( QObject *couldBeFilter, QPluginLoader::staticInstances() )

  {

    FilterInterface *filter = qobject_cast<FilterInterface*>( couldBeFilter );

    if( filter )

    {

      foreach( QString name, filter->names() )

      {

        filters[ name ] = filter;

        ui.filterList->addItem( name );

      }

    }

  }

  QDir path( "./plugins" );

  foreach( QString filename, path.entryList(QDir::Files) )

  {

    QPluginLoader loader( path.absoluteFilePath( filename ) );

    QObject *couldBeFilter = loader.instance();

    if( couldBeFilter )

    {

      FilterInterface *filter = qobject_cast<FilterInterface*>( couldBeFilter );

      if( filter )

      {

        foreach( QString name, filter->names() )

        {

          filters[ name ]  = filter;

          ui.filterList->addItem( name );

        }

      }

    }

  }

}

当执行实际的过滤操作时,必须将过滤器的名称传递给filter方法(这是从清单 11-34 中显示的filterChanged槽中处理的——清单中突出显示了小的变化)。将该清单与清单 11-25 进行比较,看看有什么不同。

清单 11-34。 将过滤器的名称传递给 filter 方法

void FilterDialog::filterChanged( QString filter )

{

  if( filter.isEmpty() )

  {

    ui.filteredLabel->setPixmap( *(ui.originalLabel->pixmap() ) );

  }

  else

  {

    QImage filtered = filters[ filter ]->filter( filter,

      ui.originalLabel->pixmap()->toImage() );

    ui.filteredLabel->setPixmap( QPixmap::fromImage( filtered ) );

  }

}

通过对界面进行这些最小的改动,你可以在一个文件中打包几个插件。将这个过程的开发成本与潜在的部署问题进行比较,当您不得不管理带有一个插件的多个文件时,可能会出现这些问题。

非 Qt 插件

几乎所有的插件技术都是通过根据目标平台的标准方法创建一个 DLL 来工作的。这样的库公开了可以用函数指针解析和引用的 C 符号。甚至 Qt 也使用这种方法,但是将其包装在易于使用的类中。如果你在 Windows 平台上使用 Dependency Walker(http://www.dependencywalker.com的免费工具)打开本章前面的 ASCII 艺术图像格式插件(你可以在基于 GCC 的平台上使用objdump工具),你可以看到两个导出的符号:qt_plugin_instanceqt_plugin_query_verification_data。(图 11-7 中显示了该工具的截图。)在内部,QPluginLoader使用QLibrary类来连接导出到 DLL 的 C 符号。


注意动态链接库也可以称为共享库(以及 DLL)。


当你想为其他应用程序或你的应用程序的早期非基于 Qt 的版本构建插件支持时,知道如何在较低的级别处理插件是很重要的。这一节向您展示了它是如何完成的,以及如何使用 Qt 来访问为其他应用程序设计的插件或使用其他工具。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-7。 从依赖行者看到的一个 Qt 图片格式插件

让我们来看看您将要连接的简单库的源代码。清单 11-35 展示了sum函数的实现。该函数所做的只是计算给定数据流的校验和。

清单 11-35。sum功能全盛

int sum( int len, char *data )

{

  int i;

  int sum = 0x5a;

  for( i=0; i<len; ++i )

    sum ^= data[i];

  return sum;

}

在 Windows 平台上,我使用清单 11-36 中显示的自定义Makefile来构建一个 DLL。如果您使用另一个平台,您应该更改结果文件的文件扩展名(清单中显示的文件中的sum.dll)。在 Unix 上扩展名通常是.so,在 Mac OS 上是.dylib。有时,如果文件用作特定应用程序的插件,则使用完全自定义的扩展名。

清单 11-36。 一个 Makefile 用于构建 dll

all: sum.dll

sum.o: sum.c

  gcc -c sum.c

sum.dll: sum.o

  gcc -shared  o sum.dll sum.o

clean:

  @del sum.o

  @del sum.dll

如果您在构建 DLL 时不得不处理它的文件扩展名,那么当您尝试使用QLibrary加载它时,Qt 会帮您解决这个麻烦。该类首先尝试加载与指定名称完全相同的库。如果失败,它会在放弃之前尝试使用特定于平台的文件扩展名。

清单 11-37 展示了如何使用QLibrary来加载sum DLL。库本身位于应用程序工作目录下的lib目录中。

使用QLibrary时的工作顺序是loadisLoadedresolve。在清单中,DLL 的文件名——没有文件扩展名——在QLibrary对象的构造器中指定(也可以用setFileName方法设置)。设置好文件名后,调用load,然后用isLoaded测试加载操作的结果。如果isLoaded返回false,说明出了问题,库无法加载。这个问题有几个原因:例如,可能找不到文件或文件已损坏。

当库被加载时,是时候尝试解析您想要使用的符号了。在这种情况下,调用resolve并将字符串sum作为参数传递。您必须将来自void*的结果指针转换为适当的函数指针类型(在清单中,该类型为SumFunction)。如果返回的指针为空指针,则符号无法解析;否则,可以免费使用。

清单 11-37 中成功加载的库和解析的符号的结果是字符串sum of 'Qt Rocks!' = 56

清单 11-37。 使用 QLibrary 加载、查找、使用 sum

typedef int (*SumFunction)(int,char*);

int main( int argc, char **argv )

{

  QLibrary library( "lib/sum" );

  library.load();

  if( !library.isLoaded() )

  {

    qDebug() << "Cannot load library.";

    return 0;

  }

  SumFunction sum = (SumFunction)library.resolve( "sum" );

  if( sum )

    qDebug() << "sum of 'Qt Rocks!' = " << sum( 9, "Qt Rocks!" );

  return 0;

}

使用QLibrary和让 Qt 帮你做插件的主要区别是什么?对于初学者来说,QPluginLoader通过查看插件是在正确的平台上使用正确的工具构建的,来确保插件能够与 Qt 应用程序一起工作。QPluginLoader也让你可以访问一个类实例,而不是一组可以用来创建类实例的 C 符号。

另一方面,QLibrary使你能够使用没有 Qt 构建的插件。您还可以使您的 Qt 应用程序适应旧的非 Qt 规范。

当您必须使用QLibrary时,我建议您将代码隐藏在单个类中。这样,您就可以包含该类中的复杂性,并在应用程序的其余部分保持面向对象的 Qt 风格。

总结

Qt 使得处理插件变得容易。通过继承和实现一个接口类,可以扩展 Qt 来处理定制的数据库驱动程序、图像格式,甚至窗口装饰样式。你也可以用插件来扩展你自己的应用,要么让 Qt 处理插件接口,要么通过一个底层接口。

如果你需要连接为其他应用程序制作的插件或者根据标准定义的插件,你可以使用QLibrary类对 dll 进行底层访问。这个类使得接口几乎任何代码成为可能。

让 Qt 通过QPluginLoader类结合Q_DECLARE_INTERFACEQ_EXPORT_PLUGINQ_INTERFACES宏和QObject类来处理插件更容易。

当创建新的插件接口时,构建持久的接口是很重要的。尽可能让接口通用化,让它们像工厂一样工作。能够将几个插件放在一个插件中可以大大简化部署。

如果您计划在应用程序中使用插件,您可以为插件使用与内部功能相同的接口。只需将您希望成为应用程序一部分的基本功能转换成静态链接的插件。这样,从应用程序的角度来看,您只需要担心一个接口,并且您仍然可以将功能放在您的可执行文件中。*******

十二、并行

在编写软件时,你经常会遇到一大块工作必须执行的情况。如果在图形应用程序中书写,图形用户界面有时会冻结。幸运的是,使用线程时可以避免这种情况。

每个应用程序通常作为进程运行。在大多数现代操作系统中,几个应用程序可以同时运行,这意味着几个任务正在并行执行。这两个过程是分离的,互不相关。

在每个进程内部,可以有一个或多个线程在运行。这些线程共享资源和内存,并且需要相互了解。他们也可以合作完成任务,分担繁重的工作。这也有助于多处理器系统高效地工作,因为单个应用程序可以拆分到几个处理器上。

回到最初的问题——用户界面冻结——线程会有所帮助。通过在单独的线程中执行之前冻结应用程序的大量工作,主线程可以专注于更新和响应来自用户界面的事件。

处理器之间线程和进程的分配,以及进程和线程之间的切换,都是由底层操作系统来处理的,所以线程化是一个非常依赖于平台的话题。Qt 提供了线程和进程的公共类,以及让它们协作和共享数据的工具。然而,不同平台的执行顺序、速度和优先级都有所不同,因此在应用中实现线程化时必须格外小心。

基本穿线

让我们先来看看 Qt 的线程类,看看如何使用 Qt 开始使用线程。

重要的是要理解,一旦应用程序启动,它实际上是作为一个线程运行的,称为主线程。这意味着对QApplication::exec方法的调用是从主线程发出的,而QApplication对象驻留在该线程中。主线程有时被称为图形用户界面(GUI)线程,因为所有的窗口小部件和其他用户界面对象都必须由这个线程处理。

主线程通常由一个event循环和一组在该线程中创建的对象组成。通过子类化 Qt QThread类,您可以创建具有自己的event循环和对象的新线程。QThread类代表一个执行在run方法中实现的工作的线程。通过为您的线程实现一个定制的run方法,您已经创建了一个独立于主线程的线程,可以执行它的任务。

构建简单的线程应用程序

清单 12-1 展示了一个类的类声明,它实现了一个名为TextThread的独立线程。您可以看出该类实现了一个单独的线程,因为它继承了QThread类。当这样做时,也有必要实现run方法。

线程的构造器接受一个文本字符串,然后在运行时每秒向调试控制台输出一次该文本。

清单 12-1。TextThread类声明

class TextThread : public QThread

{

public:

  TextThread( const QString &text );

  void run();

private:

  QString m_text;

};

清单 12-2 中的实现了TextThread类。首先有一个全局变量stopThreads,用于停止所有线程的执行。通过使用terminate方法可以停止一个线程,但是这可以与让一个线程崩溃相比较。什么都不清理,不保证成功。

在构造器中,给定的文本被注意到并存储在文本线程的私有成员中。确保调用QThread构造器,以便正确初始化线程。

run方法中,当stopThreads被设置为true时,执行进入一个循环。在循环中,在线程使用sleep方法休眠至少一秒钟之前,使用qDebug将文本发送到调试控制台。注意sleep让线程等待至少指定的时间。这意味着睡眠可以持续比指定时间更长的时间,并且在调用sleep之间花费的睡眠时间可以不同。


提示sleep方法可以让你暂停一个线程几秒钟。用msleep,可以用毫秒(千分之一秒)来指定休眠周期;使用usleep,你可以用微秒(百万分之一秒)来指定睡眠时间。睡眠的可能最短持续时间由硬件和当前软件平台决定。由于这些限制,请求睡眠一微秒很可能会导致更长的睡眠时间。


清单 12-2。TextThread类实现和全局变量 stopThreads

bool stopThreads = false;

TextThread::TextThread( const QString &text ) : QThread()

{

  m_text = text;

}

void TextThread::run()

{

  while( !stopThreads )

  {

    qDebug() << m_text;

    sleep( 1 );

  }

}

在清单 12-3 中,TextThread类用于实例化两个对象,只要对话框打开,这两个对象就会启动并保持运行。当用户关闭对话框时,stopThreads标志被设置为true,在退出main函数之前,您等待线程实现这一点。这种等待可能长达一秒钟,因为当标志改变时,线程可能正在休眠。

清单 12-3。 一个应用使用了 TextThread

int main( int argc, char **argv )

{

  QApplication app( argc, argv );

  TextThread foo( "Foo" ), bar( "Bar" );

  foo.start();

  bar.start();

  QMessageBox::information( 0, "Threading", "Close me to stop!" );

  stopThreads = true;

  foo.wait();

  bar.wait();

  return 0;

}

main函数中,线程对象就像任何其他对象一样被创建。然后使用start方法启动线程。当线程预计要停止时,主线程通过为每个线程调用wait方法来等待它们。您可以通过给wait()一个以毫秒为单位的时间限制,在特定的时间间隔后强制线程停止。否则,不传递参数会导致应用程序一直等到线程停止。当wait调用返回时,您可以使用isFinishedisRunning方法来确定wait调用是否超时,或者线程是否完成并停止执行。

强制线程终止

如果一个线程停止失败,可以调用terminate强行结束它的执行。请记住,这很可能会导致内存泄漏和其他问题。如果你使用一个保护标志比如stopThreads或者为每个线程实现一个stopMe槽,你就可以强制线程停止,而不必依赖于强力方法比如terminate。唯一不起作用的时候是线程挂起的时候——这时你正在处理一个应该解决的软件错误。

运行线程化应用

运行应用程序时,您会看到输出"Foo""Bar"成对出现,如清单 12-4 中的所示。有时顺序会改变,这样"Foo"会出现在"Bar"之前,反之亦然,因为sleep调用会让线程休眠至少一秒钟,操作系统可以以不同于线程休眠时的顺序唤醒线程。

这个结果展示了使用线程时的许多陷阱之一:您永远不能假设任何事情;如果您这样做,在其他平台上的行为可能会略有不同。重要的是只依赖 Qt 文档中的保证——别无其他。

清单 12-4。TextThread的试运行

"Foo"

"Bar"

"Bar"

"Foo"

"Bar"

"Foo"

"Bar"

"Foo"

"Bar"

"Foo"

"Foo"

"Bar"

"Bar"

"Foo"

安全同步

有时候你需要让两个或者更多的线程关注其他线程在做什么。这被称为同步线程,这可能发生在一个线程使用另一个线程的结果时;然后,第一个线程需要等待,直到另一个线程实际上已经产生了可以处理的东西。另一个常见的场景是几个线程共享一个公共资源;它们都需要确保没有其他线程同时使用相同的资源。

为了同步线程,你可以使用一个叫做互斥的特殊锁,它可以被锁定和解锁。如果一个不同的线程试图锁定一个已经锁定的互斥体,它将不得不等待直到它被当前的持有者解锁,然后才能锁定它。据说方法阻塞直到它能被完成。锁定和解锁操作是原子的,这意味着它们被视为单个不可见的操作,在执行过程中不能被中断。这很重要,因为锁定互斥体是一个两步过程。首先,线程检查互斥锁没有被锁定;然后将它标记为锁定。如果第一个线程在检查后被中断,然后第二个线程检查并锁定互斥体,第一个线程将认为互斥体在恢复时被解锁。然后,它会将一个已经锁定的互斥体标记为已锁定,这就造成了两个线程都认为它们已经锁定了互斥体的情况。因为锁定操作是原子的,第一个线程在检查和锁定之间不会被中断,因此第二个线程将检查并找到一个锁定的互斥体。

在 Qt 中,互斥是由QMutex类实现的。锁定和解锁的方法称为lockunlock。另一种方法tryLock,仅当互斥体不属于另一个线程时才锁定互斥体。

通过修改清单 12-1 、 12-2 和 12-3 中的应用程序,您可以确保"Foo""Bar"文本总是以相同的顺序出现。清单 12-5 显示了修改后的run方法。添加的代码行已经突出显示。

添加的行确保每个线程在打印文本和睡眠时持有锁。在此期间,另一个线程也调用lock,然后阻塞,直到当前持有者解锁互斥体。

必须添加if语句,因为main函数可能会在线程阻塞lock调用时开始关闭。如果它不在那里,被阻塞的线程会在意识到stopThreadstrue之前输出一次过多的文本。

清单 12-5。 新的 run 方法用互斥量进行排序

QMutex mutex;

void TextThread::run()

{

  while( !stopThreads )

  {

    mutex.lock();

    if( stopThreads ){

      mutex.unlock();

      return;

    }

    qDebug() << m_text;

    sleep( 1 );

    mutex.unlock();

  }

}

再次运行这个例子,你会看到"Foo""Bar"每秒打印一次,并且总是以相同的顺序。这使得原始应用程序的速度减半,在原始应用程序中,"Foo""Bar"都是每秒打印一次。不能保证哪一个文本先被打印出来— bar可能比foo更快初始化,即使start先被foo调用。订单也不能保证。通过增加执行线程的系统的工作负载或缩短睡眠时间,顺序可以改变。它之所以有效,是因为解锁互斥体的线程到达lock调用和阻塞需要不到一秒钟的时间。


提示保证线程的顺序是可能的,但是它需要两个互斥体和对run方法的更大改变。


保护您的数据

互斥不是为了保证线程的顺序;当几个线程试图同时访问数据时,它们保护数据不被破坏。

在详细了解这一点之前,您需要了解实际问题是什么。例如,考虑表达式n += 5。计算机可能会分三步执行:

  1. 从存储器中读取n
  2. 5加到数值上。
  3. 将值写回到存储n的存储器中。

如果两个线程试图同时执行该语句,顺序可能会如下所示:

  1. 线程 A 读取n的原始值。
  2. 线程 A 将5加到该值上。
  3. 操作系统切换到线程 b。
  4. 线程 B 读取n的原始值。
  5. 线程 B 将5加到该值上。
  6. 线程 B 将该值写回到存储n的内存中。
  7. 操作系统切换到线程 a。
  8. 线程 A 将该值写回到存储n的内存中。

前面描述的执行结果将是线程 A 和 B 都将值n+5存储在内存中,并且线程 A 覆盖线程 B 写入的值。结果是n的值不正确(它应该是n+10,但它是n+5)。

通过使用互斥体来保护n,当线程 A 正在处理它时,你可以防止线程 B 到达该值,反之亦然。一个线程阻塞,而另一个线程工作,因此代码的关键部分是串行执行,而不是并行执行。通过保护类中所有潜在的关键部分不被并行访问,可以从多个线程中安全地调用这些对象。据说这个类是线程安全的。

受保护计数

让线程通过一个TextDevice对象操作,而不是让TextThread线程直接向qDebug写文本。它被称为文本设备,因为它模拟了打印文本的共享设备。要使用设备打印文本,使用write方法,它将给定的文本写入调试控制台。它还列举了所有文本,这样您就可以知道write方法被调用了多少次。

在清单 12-6 的中可以看到TextDevice类声明。该类包含了您所期望的内容:一个构造器,一个write方法,一个用于枚举调用的计数器,以及一个用于保护计数器的QMutex

清单 12-6。TextDevice类声明

class TextDevice

{

public:

  TextDevice();

  void write( const QString& );

private:

  int count;

  QMutex mutex;

};

TextDevice类的实现展示了一个新技巧。清单 12-7 展示了如何使用QMutexLocker类来锁定互斥体。互斥锁一构造就锁定互斥体,然后在互斥体被析构时解锁互斥体。

您可以选择显式调用lockunlock的解决方案,但是通过使用QMutexLocker,您可以确保互斥体被解锁,即使您在方法中途退出return语句或者到达方法末尾时也是如此。结果是write方法不能从不同的线程进入两次——调用将被序列化。

清单 12-7。TextDevice类实现

TextDevice::TextDevice()

{

  count = 0;

}

void TextDevice::write( const QString& text )

{

  QMutexLocker locker( &mutex );

  qDebug() << QString( "Call %1: %2" ).arg( count++ ).arg( text );

}

TextThread class’ run方法与原来的清单 12-2 相比变化不大。现在调用的是write方法而不是qDebug。清单 12-8 中突出显示了这一变化。

m_device成员变量是指向要使用的TextDevice对象的指针。它是从构造器中的给定指针初始化的。

清单 12-8。TextThread::run方法现在调用 write ,而不是直接输出到 qDebug

void TextThread::run()

{

  while( !stopThreads )

  {

    m_device->write( m_text );

    sleep( 1 );

  }

}

与您在清单 12-3 中看到的相比,main函数也做了轻微的修改。新版本创建了一个在TextThread线程对象上传递的TextDevice对象。新版本可以在清单 12-9 中看到,其中的变化被突出显示。

清单 12-9。 一个 TextDevice 对象被实例化并传递给 TextThread 线程对象

int main( int argc, char **argv )

{

  QApplication app( argc, argv );

  TextDevice device;

  TextThread foo( "Foo", &device ), bar( "Bar", &device );

  foo.start();

  bar.start();

  QMessageBox::information( 0, "Threading", "Close me to stop!" );

  stopThreads = true;

  foo.wait();

  bar.wait();

  return 0;

}

构建和执行应用程序会产生一个编号为"Foo""Bar"的文本列表(在清单 12-10 中可以看到一个例子)。输出的顺序是不确定的,但是枚举总是有效的——这要感谢保护计数器的互斥体。

清单 12-10。 一次试运行的计数 TextDevice

"Call 0: Foo"

"Call 1: Bar"

"Call 2: Bar"

"Call 3: Foo"

"Call 4: Bar"

"Call 5: Foo"

"Call 6: Bar"

"Call 7: Foo"

"Call 8: Bar"

"Call 9: Foo"

"Call 10: Bar"

"Call 11: Foo"
锁定读写

使用互斥体来保护变量有时会导致潜在的性能下降。两个线程可以同时读取共享变量的值而不锁定它,但是如果第三个线程进入场景并试图更新该变量,它必须锁定它。

为了处理这种情况,Qt 提供了QReadWriteLock类。这个类的工作很像QMutex,但是它提供了lockForReadlockForWrite方法,而不是lock方法。就像使用QMutex一样,你可以直接使用这些方法,也可以使用QReadLockerQWriteLocker类,它们在构造时锁定一个QReadWriteLock,在被析构时解锁它。

让我们尝试在应用程序中使用QReadWriteLock。您将更改TextDevice的行为,这样计数器就不会从write方法更新,而是从一个名为increase的新方法更新。TextThread对象仍将在那里调用write,但是您将添加另一个线程类来增加计数器。这个名为IncreaseThread的类只是每隔一段时间调用一个给定的TextDevice对象的increase

让我们先看看新的TextDevice类的类声明,如清单 12-11 所示。与清单 12-6 中的代码相比,QMutex被一个QReadWriteLock取代,并且在接口中加入了increase方法。

清单 12-11。TextDevice类声明带 QReadWriteLock

class TextDevice

{

public:

  TextDevice();

  void increase();

  void write( const QString& );

private:

  int count;

  QReadWriteLock lock;

};

在清单 12-12 所示的实现中,您可以看到对TextDevice类所做的更改。新方法increase在改变计数器之前创建一个引用QReadWriteLockQWriteLocker。更新后的write方法在创建发送到调试控制台的文本时,在使用计数器之前,以同样的方式创建一个QReadLocker。尽管新实现的保护功能是一个相当复杂的概念,但代码相当容易阅读和理解。

清单 12-12。TextDevice类实现使用 QReadLocker QWriteLocker 来保护 count 成员变量

TextDevice::TextDevice()

{

  count = 0;

}

void TextDevice::increase()

{

  QWriteLocker locker( &lock );

  count++;

}

void TextDevice::write( const QString& text )

{

  QReadLocker locker( &lock );

  qDebug() << QString( "Call %1: %2" ).arg( count ).arg( text );

}

IncreaseThread类与TextThread类有许多相似之处(类声明如清单 12-13 中的所示)。因为它是一个线程,所以它继承了QThread。构造器接受一个指向TextDevice对象的指针来调用increase,这个类包含一个指向这样一个设备(名为m_device)的私有指针来保存这个指针。

清单 12-13。IncreaseThread类声明

class IncreaseThread : public QThread

{

public:

  IncreaseThread( TextDevice *device );

  void run();

private:

  TextDevice *m_device;

};

IncreaseThread类的实现反映了你从类声明中学到的东西(你可以在清单 12-14 中看到代码)。在构造器中初始化m_device,调用QThread构造器初始化基类。

run方法中,每 1.2 秒调用一次m_deviceincrease方法,当stopThreads置为true时循环停止。

清单 12-14。IncreaseThread类实现

IncreaseThread::IncreaseThread( TextDevice *device ) : QThread()

{

  m_device = device;

}

void IncreaseThread::run()

{

  while( !stopThreads )

  {

    msleep( 1200 );

    m_device->increase();

  }

}

TextDevice类不受这些变化的影响,它与清单 12-8 中显示的类相同。main函数也非常类似于前面的例子。唯一的变化是添加了一个IncreaseThread对象。清单 12-15 显示了main函数,并突出显示了添加的行。

清单 12-15。main功能,设置了一个 TextDevice和一个 IncreaseThread

int main( int argc, char **argv )

{

  QApplication app( argc, argv );

  TextDevice device;

  IncreaseThread inc( &device );

  TextThread foo( "Foo", &device ), bar( "Bar", &device );

  foo.start();

  bar.start();

  inc.start();

  QMessageBox::information( 0, "Threading", "Close me to stop!" );

  stopThreads = true;

  foo.wait();

  bar.wait();

  inc.wait();

  return 0;

}

应用程序输出可以在清单 12-16 中看到。"Foo""Bar"文本的顺序可以随时改变,计数器以稍微不同的间隔更新,因此有时您会得到具有相同计数器值的四个字符串;有时你会得到两根弦。在某些情况下,您可能最终得到一个带有一个计数器值的单个"Foo""Bar"(或者三个——如果IncreaseThread碰巧在来自TextThread对象的两个write调用之间调用increase)。

清单 12-16。TextDevice用单独的 increase 方法运行

"Call 0: Foo"

"Call 0: Bar"

"Call 0: Foo"

"Call 0: Bar"

"Call 1: Bar"

"Call 1: Foo"

"Call 2: Bar"

"Call 2: Foo"

"Call 3: Bar"

"Call 3: Foo"

"Call 4: Bar"

"Call 4: Foo"

"Call 4: Foo"

"Call 4: Bar"

"Call 5: Bar"

"Call 5: Foo"

线程间共享资源

当访问需要序列化时,互斥锁和读写锁有利于保护共享变量和其他共享项。有时,您的线程不仅需要共享一个变量,还需要共享有限数量的资源,如缓冲区的字节数。这就是信号量的用武之地。

信号量可以看作是计数互斥量,互斥量可以看作是二元信号量。它们实际上是一回事,但是信号量是用一个值而不是一个锁位来初始化的。当您锁定一个互斥体时,您从信号量中获取一个值,这会减少信号量的值。信号量的值永远不能小于零,因此,如果一个线程试图获取比信号量更多的资源,该线程将一直阻塞,直到请求的数量可用。当您完成获取的值时,您将它释放回信号量,这增加了信号量的值。通过释放,可以增加信号量的值,使其超过信号量的初始值。

Qt 类QSemaphore实现了信号量特性。您可以使用acquire方法从信号量对象获取一个值,或者如果您不想在请求的值不可用时阻塞,可以使用tryAcquire方法。如果采集成功,则tryAcquire方法返回true,如果请求的数量不可用,则返回false。使用release方法将一个值释放回信号量对象。如果想在不影响信号量的情况下知道信号量对象的值,可以使用available方法。如果信号量表示共享资源的可用性,并且您希望向用户显示有多少资源正在被使用,这将非常方便。

在清单 12-17 中,您可以看到当信号量对象被使用时,可用值是如何变化的。在进行一系列的acquirerelease调用之前,信号量被初始化为值10。突出显示的行显示了对tryAcquire的方法调用失败,因为该调用试图获取比可用的更多的内容。因为调用失败,信号量的可用值保持不变。

清单 12-17。 信号量的可用值因为对象被使用而改变。

QSemaphore s( 10 );

s.acquire();      // s.available() = 9

s.acquire(5);     // s.available() = 4

s.release(2);     // s.available() = 6

s.release();      // s.available() = 7

s.release(5);     // s.available() = 12

s.tryAcquire(15); // s.available() = 12
陷入困境

实现线程系统的最大风险之一是死锁,当两个线程互相阻塞从而都阻塞时,就会发生死锁。因为两个线程都被阻塞,所以都不能释放被另一个线程阻塞的资源。结果是系统冻结。


注意即使只有一个线程,也会发生死锁。想象一个线程试图从一个信号量中获取一个高于可能值的值。


用来形象化这一点的一个最常见的例子是哲学家进餐的问题。图 12-1 展示了一张桌子,五个哲学家坐在上面吃饭。每个人都有一个盘子,盘子的两边都有筷子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-1。 哲学家们正准备开饭。

哲学家使用的吃饭算法分为五个步骤:

  1. 拿起左边的筷子。
  2. 获得合适的筷子。
  3. 吃吧。
  4. 松开右边的筷子。
  5. 松开左边的筷子。

因为所有的哲学家都一样饿,所以他们都立刻拿起左边的筷子。问题是,一个哲学家的左筷子是另一个哲学家的右筷子。因此,当他们试图获得正确的筷子时,他们都会受阻。出现了僵局,他们都饿死了。

如你所见,死锁是危险的,甚至是致命的。那么,它们是如何避免的呢?第一个任务是识别可能发生死锁的潜在危险情况。寻找竞争多个资源的线程,这些线程也在不同的时间获取这些资源。如果每个哲学家都试图在一次操作中获得两只筷子,这个问题就永远不会发生。

当发现潜在的危险情况时,必须消除它。通过不盲目地获得第二根筷子,而是尝试去获得它,可以避免一次阻碍。如果拿不到第二根筷子,松开第一根也很重要,这样可以避免挡住邻居。当错过第二根筷子并返回第一根筷子时,最好的行动是睡一会儿,让相邻的哲学家吃完饭,然后再试图获得两根筷子。这将大致转化为以下算法:

  1. 拿起左边的筷子。
  2. 试着找到合适的筷子。
  3. 如果两个操纵杆都已获得,继续步骤 6。
  4. 松开左边的筷子。
  5. 在继续第一步之前思考一会儿。
  6. 吃吧。
  7. 松开右边的筷子。
  8. 松开左边的筷子。

这种进食算法在最坏的情况下可以饿死最多三个哲学家,但至少其中两个会得到食物——避免了僵局。因为对于所有五个哲学家来说,得到两根筷子的概率是相等的,所以在现实生活中,五个哲学家会时不时地吃点东西。

生产者和消费者

信号量派上用场的一个常见线程场景是,一个或多个线程产生数据,一个或多个线程消耗数据。这些线程被称为生产者消费者

通常生产者和消费者共享一个用来发送信息的缓冲区。通过让一个信号量跟踪缓冲区中的空闲空间,让另一个信号量跟踪缓冲区中的可用数据,可以让使用者并行工作,直到缓冲区满了或空了(生产者或使用者必须停下来等待,直到有更多的空闲空间或数据可用)。

通过共享循环缓冲区传递数据

为了展示如何使用信号量,您将创建一个由生产者和消费者组成的应用程序。生产者将给定的文本通过循环缓冲区传递给消费者,消费者将把接收到的文本打印到调试控制台。

因为只有一个循环缓冲区,所以你已经将它实现为一组全局变量,如清单 12-18 所示。如果您计划使用几个缓冲区,显而易见的解决方案是在一个类中声明这些全局变量。然后使用缓冲区将每个生产者和消费者引用到该类的一个实例。

缓冲区由大小bufferSize和实际缓冲区buffer组成。因为您计划移动QChar对象,所以缓冲区就是那种类型的。缓冲区还需要两个信号量:一个用于跟踪可用的空闲空间,另一个用于跟踪可用数据项的数量。最后,有一个名为atEnd的标志,告诉消费者生产者将不再生产数据。

清单 12-18。 创建信号量监控的线程安全缓冲区的变量

const int bufferSize = 20;

QChar buffer[ bufferSize ];

QSemaphore freeSpace( bufferSize );

QSemaphore availableData( 0 );

bool atEnd = false;

缓冲器将从索引0填充到bufferSize-1,然后从0开始增加。在将一个字符放入缓冲区之前,生产者将从freeSpace信号量中获取。当字符被放入缓冲区时,生产者将释放给availableData信号量。这意味着,如果没有任何东西消耗缓冲区中的数据,缓冲区将被填充,并且availableData信号量值将等于bufferSize,生产者将无法获得任何更多的空闲空间。

应用程序中的生产者类称为TextProducer。它的构造器期望一个QString作为参数,并将字符串存储在私有成员变量m_text中。生产者的工作在清单 12-19 所示的run方法中执行。如前所述,for循环遍历文本,然后将QChar对象逐个放入缓冲区,与消费者同步。当整个文本已经被发送时,atEnd标志被设置为true,因此消费者知道整个文本已经被发送。

清单 12-19。run生产者类的方法

void TextProducer::run()

{

  for( int i=0; i<m_text.length(); ++i )

  {

    freeSpace.acquire();

    buffer[ i % bufferSize ] = m_text[ i ];

    if( i == m_text.length()-1 )

      atEnd = true;

    availableData.release();

  }

}

消费线程的读取顺序与填充顺序相同——从索引0bufferSize-1,然后再次从0开始。在读取之前,它试图从availableData信号量中获取。当从缓冲区中读取一个字符后,它会释放给freeSpace信号量,因为缓冲区的索引可以被生产者重用。

名为TextConsumer的消费者类只实现了一个run方法(参见清单 12-20 )。run方法的实现非常简单。

清单 12-20。run消费类的方法

void TextConsumer::run()

{

  int i = 0;

  while( !atEnd || availableData.available() )

  {

    availableData.acquire();

    qDebug() << buffer[ i ];

    i = (i+1) % bufferSize;

    freeSpace.release();

  }

}

当需要同步生产者和消费者以及他们对缓冲区的访问时,保持对流程发生顺序的控制是非常重要的。在数据被放入缓冲器之前,必须获取空闲空间*,并且在数据被写入缓冲器之后,必须释放可用数据。从缓冲器中取出数据也是如此——在之前获取可用数据,在之后释放空闲空间。在释放自由空间之前更新atEnd标志也是很重要的,以避免当atEnd标志为true时,消费者陷入等待可用数据信号量。使用atEnd解决方案,还必须有至少一个字节的数据要传输;不然消费者就挂了。一种解决方案是首先传输数据长度,或者最后传输数据结束标记。*

清单 12-21 显示了一个使用TextProducerTextConsumer类的main函数。它用一些虚构的拉丁文文本初始化生成器,启动两个线程,然后等待它们都完成。它们的启动顺序和等待调用的顺序是不相关的——两个线程都将使用信号量来同步自己。

清单 12-21。 main 功能使用 TextProducer TextConsumer

int main( int argc, char **argv )

{

  QApplication app( argc, argv );

  TextProducer producer( "Lorem ipsum dolor sit amet, "

                         "consectetuer adipiscing elit. "

                         "Praesent tortor." );

  TextConsumer consumer;

  producer.start();

  consumer.start();

  producer.wait();

  consumer.wait();

  return 0;

}

看前面的例子,注意到有一个与acquirerelease调用相关的性能成本。使用互斥锁和读写锁也有类似的成本,所以有时将传输的数据分成块可以提高性能。例如,将字符串作为单词而不是一个字符一个字符地发送可能会更快,这意味着在生产者线程中一次为几个字符获取空间,而不是一次一个字符,并且每次进行稍微多一点的处理。当然,这会引入性能损失,即缓冲区并不总是被充分利用,因为即使缓冲区中有空闲空间,生产者有时也会阻塞。

与竞争生产商打交道

生产者-消费者场景的一个常见版本是让几个生产者为一个消费者提供数据。例如,可以有几个工作线程为主线程提供数据。主线程是唯一可以更新用户界面的线程,因此使其成为消费者是合乎逻辑的(它也可以是生产者——一个线程可以同时是生产者和消费者)。

在将几个TextProducer对象与清单 12-20 中的TextConsumer类一起使用之前,您需要处理两个问题。第一个问题是atEnd标志,它需要被转换成信号量。它将在TextProducer构造器中被释放,并在生产者用完run方法中的数据时被获取。在消费端,while循环无法检查atEnd;用atEnd.available()代替。

第二个问题是用于写入缓冲区的索引。因为可能有几个生产者更新缓冲区,所以他们必须共享一个必须由互斥体保护的索引。

让我们看看从TextProducer类开始更新的run方法(参见清单 12-22 )。突出显示的行显示了共享索引变量index和它的互斥体indexMutex。互斥体在包含index++的行周围被锁定和解锁。那是唯一引用和更新index的地方。这里不能使用QMutexLocker,因为这会锁定整个run方法中的互斥体,并阻塞其他生产者线程。相反,互斥锁必须被锁定尽可能短的时间。

清单 12-22。TextProducer run方法,更新为处理几个同时发生的生产者

void TextProducer::run()

{

  static int index = 0;

  static QMutex indexMutex;

  for( int i=0; i<m_text.length(); ++i )

  {

    freeSpace.acquire();

    indexMutex.lock();

    buffer[ index++ % bufferSize ] = m_text[ i ];

    indexMutex.unlock();

    if( i == m_text.length()-1 )

      atEnd.acquire();

      availableData.release();

  }

}

TextConsumer类的run方法只进行了少量的更新。清单 12-23 中突出显示的一行显示了在while循环中如何使用atEnd信号量。将此与清单 12-20 中的进行比较,其中atEnd是一个标志。

清单 12-23。TextConsumer run方法,更新为同时交到几个制作人手中

void TextConsumer::run()

{

  int i = 0;

  while( atEnd.available() || availableData.available() )

  {

    availableData.acquire();

    qDebug() << buffer[ i ];

    i = (i+1) % bufferSize;

    freeSpace.release();

  }

}

请注意,在比较单个生产者版本和多个生产者版本时,生产者和消费者之间使用信号量来获取可用数据和空闲空间的实际交互是不变的。

清单 12-24 显示了一个main函数,它设置了两个生产者和一个消费者。生产者和消费者被设置和启动;然后,该函数等待它们完成,就像在单生产者版本中一样。

清单 12-24。 一个 main 函数有两个生产者和一个消费者

int main( int argc, char **argv )

{

  QApplication app( argc, argv );

  TextProducer p1( "this text is written using lower case characters."

    "it will compete with text written using upper case characters." );

  TextProducer p2( "THIS TEXT IS WRITTEN USING UPPER CASE CHARACTERS!"

    "IT WILL COMPETE WITH TEXT WRITTEN USING LOWER CASE CHARACTERS!" );

  TextConsumer consumer;

  p1.start();

  p2.start();

  consumer.start();

  p1.wait();

  p2.wait();

  consumer.wait();

  return 0;

}

尽管双生产者版本的不同执行的结果有时不同,但是有一个重复的模式。清单 12-25 显示了一次执行的结果。你可以看到小写的生产者先取得控制权,大写的生产者插队,他们转移一两次,其中一个线程领先。开始的线程随时间而变化,并且主导线程变化的次数也随时间而变化。每次重复的模式是两个线程之间的分布不均匀。一个线程总是提供大多数字符。

这种模式的原因是线程被安排在失去焦点之前运行几毫秒。当缓冲区已被填满并且生产者无法获得更多的空闲空间时,任一线程都可以在空闲空间再次出现时取得领先。

清单 12-25。 所收的人物 TextConsumer

this text is writTHteIS TEXT nIS WRITTEN USING UPP ER CASE CHARACTEuRS

!IT WILL COMPEsTE WITH TEXT WiRITTEN USING LOnWER CASE CHARACTgERS!

lower case characters.it will compete with text written using upper

case characters.

跨越线程屏障的信令

到目前为止,您一直依赖共享缓冲区在线程之间传递数据。还有一个稍微贵一点(但简单得多)的解决方案:使用信号和插槽。使用它们可以避免创建和使用缓冲区;相反,您可以在整个应用程序中使用事件驱动的范例。


提示在 Qt 4.0 之前的 Qt 版本中,无法在线程间发送信号。相反,您必须依赖于在线程之间传递自定义事件。这在 Qt 4.0 中仍然支持,但是使用信号和插槽要容易得多。


在线程间传递信号和在一个线程内使用信号是有一些区别的。当在单线程应用程序或单线程中发出信号时,发出调用直接调用所有连接的插槽,发出代码一直等待,直到插槽完成。当向另一个线程中的对象发出信号时,信号被排队。这意味着发出调用将在插槽被激活之前或同时返回。

也可以在一个线程中使用排队信号。你所需要做的就是明确地告诉connect你想要创建一个排队连接。默认情况下,connect使用线程内的直接连接和线程间的排队连接。这是最有效的选择,因此自动设置总是有效的,但是如果您指定要排队的连接,您将获得性能。

在线程间传递字符串

让我们从本章开始回到TextThreadTextDevice类。不是让文本线程调用文本设备来传递文本,而是发送一个信号。信号将从文本线程传递到主线程中的文本设备。

新的TextThread类可以在清单 12-26 中看到。突出显示的行显示了添加信号和stop方法所做的更改。

在早期版本中,该类依赖于一个全局标志变量,该变量指示线程应该暂停执行;在这个版本中,标志m_stop是内部的,使用stop方法设置。

为了允许信号,添加了Q_OBJECT宏,以及一个signals部分和一个实际信号writeText,带有一个QString作为参数。

清单 12-26。TextThread writeText 信号

class TextThread : public QThread

{

  Q_OBJECT

public:

  TextThread( const QString& text );

  void run();

  void stop();

signals:

  void writeText( const QString& );

private:

  QString m_text;

  bool m_stop;

};

TextDevice类已经变成了一个线程——它现在继承了QThread,并且拥有与TextThread类相同的停止机制。(类声明可以在清单 12-27 中看到。)突出显示的行显示了Q_OBJECT宏、public slots部分和接受QString作为参数的实际插槽(write)。

清单 12-27。 TextDevice 类声明为线程

class TextDevice : public QThread {   Q_OBJECT`
public:
  TextDevice();

void run();
  void stop();

public slots:
  void write( const QString& text );

private:
  int m_count;
  QMutex m_mutex;
};`

清单 12-28 显示了TextThread类的完整实现。这三个方法体看起来都很简单——事实也的确如此。构造器初始化私有成员,并将调用传递给QThread构造器。stop方法简单地将m_stop设置为truerun方法由监控所述m_stop标志的while循环组成。只要它运行,每秒钟就会发出一次携带m_text作为自变量的writeText信号。

清单 12-28。TextThread的实现

TextThread::TextThread( const QString&text ) : QThread()

{

  m_text = text;

  m_stop = false;

}

void TextThread::stop()

{

  m_stop = true;

}

void TextThread::run()

{

  while( !m_stop )

  {

    emit writeText( m_text );

    sleep( 1 );

  }

}

TextDevice run方法非常简单,因为该类在没有收到信号调用的情况下不执行任何工作。查看清单 12-29 中的,你可以看到这个方法简单地调用exec进入线程的event循环,等待信号到达。event循环一直运行,直到quit被调用(这是 stop 方法中唯一发生的事情)。

在同一清单中,您还可以看到write插槽的实现。因为这个插槽可以同时被几个线程调用,所以它使用一个互斥体来保护m_count计数器。该槽可以作为函数直接调用,也可以被发出的信号调用,所以不能因为信号被一个接一个地排队和服务就忘记这一点。

清单 12-29。write槽和 TextDevice run 方法

void TextDevice::run()

{

  exec();

}

void TextDevice::stop()

{

  quit();

}

void TextDevice::write( QString text )

{

  QMutexLocker locker( &m_mutex );

  qDebug() << QString( "Call %1: %2" ).arg( m_count++ ).arg( text );

}

使用TextThreadTextDevice类很简单。请看清单 12-30 中的main函数设置两个文本线程和一个设备的例子。

因为数据是通过信号和插槽交换的,所以不同的线程对象不需要知道彼此;它们只是通过对connect的两个调用相互连接。当连接建立后,它们将被启动,并显示一个对话框。一旦对话框关闭,所有三个线程都将停止。然后,该函数在应用程序结束之前等待它们真正停止。

清单 12-30。 main 功能使用 TextThread TextDevice

int main( int argc, char **argv )

{

  QApplication app( argc, argv );

  TextDevice device;

  TextThread foo( "Foo" ), bar( "Bar" );

  QObject::connect( &foo, SIGNAL(writeText(const QString&)),

                    &device, SLOT(write(const QString&)) );

  QObject::connect( &bar, SIGNAL(writeText(const QString&)),

                    &device, SLOT(write(const QString&)) );

  foo.start();

  bar.start();

  device.start();

  QMessageBox::information( 0, "Threading", "Close me to stop!" );

  foo.stop();

  bar.stop();

  device.stop();

  foo.wait();

  bar.wait();

  device.wait();

  return 0;

}

运行这个应用程序会得到类似于清单 12-10 中所示的结果:一个编号字符串的列表。

在线程间发送你自己的类型

无需任何额外的工作,您就可以通过排队连接发送各种类的对象,如QStringQImageQVariant等等。在某些情况下,您应该在连接中使用自己的类型。这实际上是很常见的,因为大多数应用程序都包含一个或多个自定义类型,这些类型很容易与信号一起传递。

如果你试图通过一个排队的连接传递一个自定义类型,你将会遇到运行时错误,看起来非常类似于清单 12-31 中显示的错误。由于信号及其参数的排队方式,在建立和引发连接时会出现错误。

清单 12-31。 试图通过排队连接传递自定义类型

QObject::connect: Cannot queue arguments of type 'TextAndNumber'

(Make sure 'TextAndNumber' is registed using qRegisterMetaType().)

QObject::connect: Cannot queue arguments of type 'TextAndNumber'

(Make sure 'TextAndNumber' is registed using qRegisterMetaType().)

当一个信号被排队时,它和它的参数一起排队。这意味着参数在传递到插槽之前被复制并存储在一个队列中。为了能够对一个参数进行排队,Qt 需要构造、析构和复制这样一个对象。

为了让 Qt 知道如何做到这一点,所有定制类型都需要使用qRegisterMetaType进行注册,就像错误消息所说的那样。让我们看看现实生活中是如何做到这一点的。

首先,你需要一些背景知识,了解你想要达到的目标。在线程信号和插槽演示中,您将文本字符串从TextThread对象发送到了TextDevice对象。文本设备计算它接收到的字符串的数量。您将通过让TextThread对象记录它们发送了多少条文本来扩展这一功能。然后,它们会将包含文本及其计数的TextAndNumber对象发送到文本设备。

TextAndNumber类是将通过排队连接传递的自定义类型,它将保存一个QString和一个整数。清单 12-32 显示了它的类声明。

该类本身由两个构造器组成:一个不带参数;另一个接受文本和整数。元类型注册需要不带任何参数的构造器,而另一个构造器是为了方便起见而提供的——稍后在发出时会用到它。textnumber是公开的,所以您不需要担心它们的 setter 和 getter 方法。

要将该类用作元类型,还必须提供一个公共析构函数和一个公共复制构造器。因为这个类不包含默认版本不能处理的数据,所以不需要显式实现它们。

清单最后突出显示的一行包含对Q_DECLARE_METATYPE宏的引用。通过将类型传递给这个宏,该类型可以与QVariant对象结合使用,这是使用qRegisterMetaType注册它所必需的。

清单 12-32。TextAndNumber类声明

class TextAndNumber

{

public:

  TextAndNumber();

  TextAndNumber( int, QString );

  int number;

  QString text;

};

Q_DECLARE_METATYPE( TextAndNumber );

qRegisterMetaType的实际调用来自main函数,可以在清单 12-33 中的的第一个高亮行中看到。另外两条更改的线路是连接呼叫。自从您传递了QString对象后,它们已经发生了变化,因为信号和插槽现在都有了新的参数类型。

清单 12-33。 主函数将 TextAndNumber 注册为元类型,并为新的信号和插槽建立连接

int main( int argc, char **argv )

{

  QApplication app( argc, argv );

  qRegisterMetaType<TextAndNumber>("TextAndNumber");

  TextDevice device;

  TextThread foo( "Foo" ), bar( "Bar" );

  QObject::connect( &foo, SIGNAL(writeText(TextAndNumber)),

                    &device, SLOT(write(TextAndNumber)) );

  QObject::connect( &bar, SIGNAL(writeText(TextAndNumber)),

                    &device, SLOT(write(TextAndNumber)) );

...

}

TextDevice类的更改仅限于write槽。如清单 12-34 所示,该插槽现在接受一个TextAndNumber对象作为参数,而不是一个QString。它打印自己的计数器值、收到的文本和收到的数字。

清单 12-34。TextDevicewrite槽接受一个 TextAndNumber 对象作为自变量**

void TextDevice::write( TextAndNumber tan )

{

  QMutexLocker locker( &m_mutex );

  qDebug() << QString( "Call %1 (%3): %2" )

    .arg( m_count++ )

    .arg( tan.text )

    .arg( tan.number );

}

TextThread类得到了稍微多一点的改变,这可以在清单 12-35 中的方法中看到。首先,现在发出的信号带有一个TextAndNumber参数——这里使用了前面提到的方便的构造器。另一个变化是每个文本线程现在都有一个本地计数器,它在 emit 调用中更新,并且不受任何互斥体的保护,因为它只在一个线程中使用。

清单 12-35。TextThread run方法现在更新一个计数器并发出一个 TextAndNumber 对象而不是一个 QString

void TextThread::run()

{

  while( !m_stop )

  {

    emit writeText( TextAndNumber( m_count++, m_text ) );

    sleep( 1 );

  }

}

运行所描述的应用程序会产生类似于清单 12-36 中所示的结果。调用由TextDevice对象计数,而每个字符串的出现次数由每个TextThread对象计数。如您所见,文本线程的顺序是不受控制的。

清单 12-36。 用线程本地计数器运行文本线程应用

"Call 0 (0): Foo"

"Call 1 (0): Bar"

"Call 2 (1): Bar"

"Call 3 (1): Foo"

"Call 4 (2): Foo"

"Call 5 (2): Bar"

"Call 6 (3): Bar"

"Call 7 (3): Foo"

"Call 8 (4): Foo"

"Call 9 (4): Bar"

"Call 10 (5): Foo"

"Call 11 (5): Bar"

"Call 12 (6): Foo"

"Call 13 (6): Bar"

线程、对象和规则

在关于线程间连接的小节中,您了解到了connect调用会自动在不同线程中的对象之间创建排队连接。所有的QObject实例都知道它们属于哪个线程——据说它们具有线程相似性

有一些限制适用于QObject和线程:

  • QObject的子线程必须与QObject本身属于同一个线程。
  • 事件驱动的对象只能在一个线程中使用。
  • 所有的QObject必须在它们所属的QThread被删除之前被删除。

第一条规则意味着QThread本身不应该被用作父线程,因为它是在另一个线程中创建的。

第二条规则适用于定时器和网络套接字等机制。除了计时器或套接字的线程之外,您不能在其他线程中启动计时器或建立套接字连接,因为每个线程都有自己的event循环。如果你计划在一个线程中使用事件,你必须调用QThread::exec方法来启动线程的本地event循环。

第三个规则很容易管理:让您创建的所有对象在线程的run方法的堆栈上都有一个父对象(或祖父对象)。

理解一个QObject可以同时在几个线程中使用是很重要的——但是 Qt 提供的大多数对象都被设计成在一个线程中使用,所以你的收获可能会有所不同。

穿线时的陷阱

Qt 的一些部分很容易在单线程中使用。这并不意味着它们不能从一个QThread对象中使用,或者它们与线程化的应用程序不兼容;最好将所有这样的对象放在一个线程中。如果需要与其他线程交互,可以使用信号、插槽和管理相关对象的线程的方法来执行。

保存在一个线程中的对象类型包括整个 SQL 模块以及QTimerQTcpSocketQUdpSocketQHttpQFtpQProcess对象。

“行为不当”的一个例子是从一个线程创建一个QFtp对象,然后从另一个线程与之交互。这个过程可能会起作用,但它可能会导致神秘且难以调试的问题。为了避免寻找这些幽灵 bug,在使用线程时要小心。

用户界面线程

所有的窗口小部件和用户界面对象必须由主线程(调用QApplication::exec的线程)处理。这意味着所有用户界面都将充当某种消费者——从执行实际工作的线程那里获得可视化信息。

将应用程序分成这些部分的好处是,当应用程序遇到繁重的任务时,用户界面不会冻结。相反,当处理在另一个线程中进行时,一些QAction对象可能被禁用。当结果准备好时,它通过缓冲区、自定义事件、共享缓冲区或其他机制反馈给主线程。

文本和带有小工具的数字

为了显示一个用来自线程的数据更新的简单用户界面,您将用一个对话框替换来自TextAndNumber应用程序的TextDevice类。来自TextThread生产者的数据传递是通过信号到插槽的连接完成的。运行应用如图 12-2 中所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-2。TextDialog在行动

*对话框类的类声明可以在清单 12-37 中看到。对话框类被称为TextDialog,通过showText插槽接受TextAndNumber对象。

从类声明中可以学到更多的东西。您可以看到该对话框使用了使用 Designer 制作的设计,因为它包含一个Ui::TextDialog成员变量。它还有一个专用插槽,用于连接名为buttonClicked的用户接口信号。

清单 12-37。TextDialog类声明

class TextDialog : public QDialog

{

  Q_OBJECT

public:

  TextDialog();

public slots:

  void showText( TextAndNumber tan );

private slots:

  void buttonClicked( QAbstractButton* );

private:

  int count;

  QMutex mutex;

  Ui::TextDialog ui;

};

对话框如图图 12-2 所示,来自设计器的对象层次结构如图图 12-3 所示。列表小部件和按钮框在实际的对话框中以网格布局排列。

按钮框的关闭按钮连接到对话框的reject槽来关闭它,而重置按钮会在源代码中连接。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-3。TextDialog对象层次

*在清单 12-38 中可以看到TextDialog类的部分实现。您可以看到建立用户界面、将按钮盒连接到buttonClicked插槽并初始化计数器的构造器。

清单中还显示了buttonClicked插槽。当点击关闭和重置按钮时,该插槽被调用。通过检查抽象按钮的角色,可以确定是否单击了 Reset。在这种情况下,list 小部件将从它可能包含的任何列表项中清除。

清单 12-38。 用户界面处理部分 TextDialog

TextDialog::TextDialog() : QDialog()

{

  ui.setupUi( this );

  connect( ui.buttonBox, SIGNAL(clicked(QAbstractButton*)),

           this, SLOT(buttonClicked(QAbstractButton*)) );

  count = 0;

}

void TextDialog::buttonClicked( QAbstractButton *button )

{

  if( ui.buttonBox->buttonRole( button ) == QDialogButtonBox::ResetRole )

    ui.listWidget->clear();

}

TextDialog类实现的剩余部分是showText槽。它可以在清单 12-39 中看到,并且与清单 12-34 中显示的TextDevice类的write插槽几乎相同。这表明两个QThread对象之间的通信和QThread对象与主线程之间的通信没有区别。同样的规则适用,同样的限制仍然存在。

清单 12-39。showTextTextDialog的插槽**

void TextDialog::showText( TextAndNumber tan )

{

  QMutexLocker locker( &mutex );

  ui.listWidget->addItem( QString( "Call %1 (%3): %2" )

    .arg( count++ )

    .arg( tan.text )

    .arg( tan.number ) );

}

启动线程和显示对话框的main函数与清单 12-33 中的相比没有太大变化,除了用TextDialog代替了TextDevice。对话框现在作为线程启动,但在QApplication::exec启动前显示。当该调用返回时,TextThread线程停止并等待来自exec调用的返回值返回。

在图 12-2 中可以看到该应用程序的运行。注意,您可以在 list 小部件中上下移动,并独立于两个线程清除它;他们会在主线程中发生任何事情的同时继续添加条目。

处理流程

与线程密切相关的是进程,它可以由几个线程组成,但不像线程那样共享内存和资源。属于单个进程的线程共享内存和资源,并且都是同一应用程序的一部分。一个进程就是你通常所说的另一个应用程序。它有自己的内存和资源,过着自己的生活。Qt 通过QProcess类处理进程。

如果从应用程序中启动一个进程,则通过通道(称为标准输入、标准输出和标准错误通道)与它进行通信。这些是控制台应用程序可用的通道,数据仅限于字节流。

运行 uic

要使用使用QProcess类的进程编写文本,您将构建一个启动uic的小应用程序。uic应用程序是一个很好的玩法,因为如果你是 Qt 开发者,你就可以使用它(它和 Qt 捆绑在一起)。uic应用程序产生标准输出和标准误差的输出。它还可以处理您传递给它的一些不同的参数。

使用QProcess的应用程序由一个简单的对话框类ProcessDialog组成(参见图 12-4 )。在清单 12-40 中可以看到类声明。突出显示的行显示了与QProcess级可用信号相匹配的插槽范围。

清单 12-40。ProcessDialog类声明

class ProcessDialog : public QDialog

{

  Q_OBJECT

public:

  ProcessDialog();

private slots:

  void runUic();

  void handleError( QProcess::ProcessError );

  void handleFinish( int, QProcess::ExitStatus );

  void handleReadStandardError();

  void handleReadStandardOutput();

  void handleStarted();

  void handleStateChange( QProcess::ProcessState );

private:

  QProcess *process;

  Ui::ProcessDialog ui;

};

QProcess类发出的信号可用于监控已启动流程的进度或故障:

  • 进程遇到了某种内部错误。
  • started():流程已经开始。
  • finished( int code, QProcess::ExitStatus status ):进程已经退出。
  • readyReadStandardError():有数据要从标准错误通道读取。
  • readyReadStandardOutput():标准输出通道有数据要读取。
  • stateChanged( QProcess::ProcessState newState ):流程进入了一个新的状态。

当有数据准备读取时,您可以使用readAllStandardError方法或readAllStandardOutput方法读取,具体取决于您感兴趣的通道。使用 set standardOutputFilesetStandardErrorFile,您可以将任一通道的输出重定向到一个文件。

过程状态可以在三种状态NotRunningStartingRunning之间变化。当进入NotRunning时,你就知道这个过程已经结束或者即将结束。状态变为NotRunning后可以接收结束信号,但错误信号一般在stateChanged信号之前发出。

在您可以接收任何信号之前,您需要从runUic槽开始一个新的进程。你可以在清单 12-41 的中看到插槽实现。在创建一个新的QProcess对象和设置连接之前,非高亮显示的行禁用用户界面并清除用于显示应用程序输出的QTextEdit小部件。

突出显示的行显示了如何初始化和启动流程。首先,在调用start之前,参数被组装到一个QStringList对象中。start调用将可执行文件的名称和参数作为参数。在start方法调用之后,就是等待信号的到来。

清单 12-41。 一个 QProcess 对象被创建、连接并启动。

void ProcessDialog::runUic()

{

  ui.uicButton->setEnabled( false );

  ui.textEdit->setText( "" );

  if( process )

    delete process;

  process = new QProcess( this );

  connect( process, SIGNAL(error(QProcess::ProcessError)),

           this, SLOT(handleError(QProcess::ProcessError)) );

  connect( process, SIGNAL(finished(int,QProcess::ExitStatus)),

           this, SLOT(handleFinish(int,QProcess::ExitStatus)) );

  connect( process, SIGNAL(readyReadStandardError()),

           this, SLOT(handleReadStandardError()) );

  connect( process, SIGNAL(readyReadStandardOutput()),

           this, SLOT(handleReadStandardOutput()) );

  connect( process, SIGNAL(started()),

           this, SLOT(handleStarted()) );

  connect( process, SIGNAL(stateChanged(QProcess::ProcessState)),

           this, SLOT(handleStateChange(QProcess::ProcessState)) );

  QStringList arguments;

  arguments << "-tr" << "MYTR" << "processdialog.ui";

  process->start( "uic", arguments );

}

当信号到达时,插槽将使输出在用于显示执行结果的QTextEdit小部件中可见。因为几乎所有插槽看起来都一样,所以看一下handleFinish。你可以在清单 12-42 中看到源代码。

插槽通过一个switch语句传递枚举类型,将其转换成一个字符串。然后,它将生成的文本作为新段落以粗体追加到文本编辑中。所有粗体文本都是状态消息,而正常粗细的文本是应用程序的实际输出。

清单 12-42。handleFinish槽实现

void ProcessDialog::handleFinish( int code, QProcess::ExitStatus status )

{

  QString statusText;

  switch( status )

  {

    case QProcess::NormalExit:

      statusText = "Normal exit";

      break;

    case QProcess::CrashExit:

      statusText = "Crash exit";

      break;

  }

  ui.textEdit->append( QString( "<p><b>%1 (%2)</b><p>" )

    .arg( statusText )

    .arg( code ) );

}

运行该应用程序显示了在流程生命周期的不同阶段发出的不同信号。图 12-4 显示了成功执行的结果。发出的信号如下:

  1. stateChanged( Starting )
  2. started()
  3. readyReadStandardOutput()(几次)
  4. stateChanged( NotRunning )
  5. finished( 0, NormalExit )

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-4。uic流程成功运行并完成。顶部图像显示输出文本的顶部;底部的图像显示了同一文本的结尾。


注意您使用 append 调用将应用程序的输出添加到QTextEdit中,这将导致每个新的文本块作为一个新段落被添加。这就是为什么截图中的输出看起来有点奇怪。


图 12-5 中的运行显示了一个因失败而退出的流程。问题是启动的uic实例找不到指定的输入文件。发出的信号如下:

  1. stateChanged( Starting )
  2. started()
  3. readyReadStandardError()(可能几次)
  4. stateChanged( NotRunning )
  5. finished( 1, NormalExit )

如您所见,除了输出被发送到标准错误通道而不是标准输出通道之外,唯一真正的区别是退出代码非零。这是约定,但不保证。从QProcess对象的角度来看,执行进行得很顺利——所有问题都由启动的可执行文件处理。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-5。uic流程因出错退出;它找不到指定的输入文件。

*如果您为进程指定了一个无效的可执行文件名称,问题将会在进程启动之前出现。这导致了图 12-6 中所示的信号:

  1. stateChanged( Starting )
  2. error( FailedToStart )
  3. stateChanged( NotRunning )

该故障由QProcess对象检测,并通过error信号报告。将不会有任何完成的信号或输出要读取,因为该过程永远不会到达Running状态。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-6。 进程无法启动,因为指定的可执行文件丢失。

外壳和方向

使用流程时有几个常见的障碍。第一个原因是命令行 shell 在将参数传递给可执行文件之前对其进行了处理。例如,在 Unix shell 中编写uic *.ui会将所有匹配*.ui的文件名作为参数提供给uic。当使用QProcess启动进程时,您必须注意它并找到实际的文件名(使用一个QDir对象)。

第二个问题与第一个问题密切相关。管道由命令行 shell 管理。命令ls −l | grep foo确实意味着 shell 将−l | grep foo作为参数传递给ls,但是如果您开始使用QProcess,就会发生这种情况。相反,您必须将ls −l作为一个进程运行,并将结果数据传递给另一个运行grep foo的进程。

这就把你带到了最后一个障碍:渠道的方向。流程的标准输出是您的输入。进程所写的就是你的应用程序所读的。这也适用于标准错误通道——进程向它写入数据,以便应用程序从中读取数据。标准输入正好相反——进程从中读取数据,因此应用程序必须向其写入数据。

总结

使用线程会增加应用程序的复杂性,但会提高性能。随着多处理器系统变得越来越普遍,这一点尤其重要。

开发多线程应用程序时,必须确保不要对时间或性能做任何假设。你永远不能依赖于以一定的顺序或速度发生的事情。如果您意识到了这一点,那么开始真的很容易—只需继承QThread类并实现run方法。

使用QMutexQMutexLocker类可以很容易地保护共享资源。如果您主要是从一个值中读取,为了获得更好的性能,更好的选择是将QReadWriteLockQReadLockerQWriteLocker结合使用。对于大量使用的共享资源,QSemphore是您的最佳选择。

线程化时,必须确保QObject实例被保持在一个线程中。您可以从创建对象的线程之外的线程访问QObject的成员。只要确保保护任何共享数据。一些QObject衍生工具根本不打算共享:网络类、整个数据库模块和QProcess类。图形类更挑剔——它们必须在主线程中使用。********************************

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值