baizy77 发表于 2019-7-11 21:10:26

KS13-03 使用QtConcurrent处理并发-Map

本帖最后由 baizy77 于 2019-7-16 20:43 编辑

版权声明----------------------------------------------------------------------------------------------------------------------------该文章原创于Qter开源社区(www.qter.org)作者: 女儿叫老白转载请注明出处!----------------------------------------------------------------------------------------------------------------------------课程目录: 【独家连载】《Qt入门与提高-GUI产品开发》目录
网页版课程源码 提取码:1uy7
引言------------------------------------------------------------------------------------在并发操作中,有些处理是可以同步进行的,比如批量转换文件、批量导出数据库表、计算图形的缩略图等。这些操作相互之间没有关联,因此可以并行执行(同步与并行都是指可以同时执行)。本节为读者介绍使用QtConcurrent的Map处理并发操作。
正文------------------------------------------------------------------------------------QtConcurrent是Qt提供的用来处理多线程并发操作的模块,它可以自动根据可用的CPU核心数分配调整所用的线程数目,而且无需开发者关注内部细节。也就是说用QtConcurrent开发的程序兼容多核计算机。QtConcurrent提供了一个静态的接口用于处理可以并发执行且相互之间没有任何关联的操作,Qt提供的定义如下:代码清单13-03-01qtconcurrentmap.htemplate <typename OutputSequence, typename InputSequence, typename MapFunctor>
OutputSequence blockingMapped(const InputSequence &sequence, MapFunctor map)
{
    return blockingMappedReduced<OutputSequence>
      (sequence,
         QtPrivate::createFunctionWrapper(map),
         QtPrivate::PushBackWrapper(),
         QtConcurrent::OrderedReduce);
}
代码清单13-03-01摘自qtconcurrentmap.h。该清单所示的接口展示了模板接口blockingMapped()的定义。该接口提供了两个参数:参数1为输入序列,比如一个文件名列表或者一个待处理的图片数组等;参数2是映射函数,该函数接受sequence中一个成员作为参数;blockingMapped()接口返回值为输出序列OutputSequence。QtConcurrent会自动遍历sequence,把每一个成员作为参数传入MapFunctor接口map,然后把返回值组织为一个输出序列。在本节课的示例中,我们将实现如下功能:遍历指定目录中的所有文件,挨个计算MD5码,最后把得到的所有MD5码组成一个列表。我们介绍一下怎样使用QtConcurrent实现该功能。
首先,我们看一下单线程计算批量文件md5码
代码清单13-03-02
main.cpp#include <QApplication>
#include <QTranslator>      // 国际化
#include "qglobal.h"
#include <QLibraryInfo> // 国际化
#include <QDir>
#include <QFile>
#include <qtconcurrentmap.h>
#include <iostream>

#include "baseapi.h"

using std::cout;
using std::endl;
int main(int argc, char * argv[])
{
      // ......
    QStringList strFilters;
    strFilters << "*";
    // 得到待计算MD5码的文件列表.
    QString strScanPath = ns_train::getPath("$TRAINDEVHOME");
    qDebug() << strScanPath;
QStringList files =
                ns_train::getFileList(strScanPath, strFilters, true);
    QStringList::iterator ite;
   
    int singleThreadTime = 0; // 单线程计算md5码所需的时间。
    QTime time;
    time.start();
    QList<QByteArray> md5_a;
    { // 串行操作
      QString strFileName;
      for ( ite = files.begin(); ite != files.end(); ite++) {
            md5_a.push_back(ns_train::getMd5(*ite));
      }
      singleThreadTime = time.elapsed(); // ms
      qDebug() << "single thread" << singleThreadTime;
    }   
// ......
}
在代码清单13-03-02中,
第7行,包含“qtconcurrentmap.h”,因为本示例用到的接口在该文件中定义;
第17~23行,遍历得到"$TRAINDEVHOME"目录下的所有文件(含所有子目录)并将文件列表保存到files变量;
第27~28行,为了对比使用单线程计算md5码与QtConcurrent计算md5码的速度,定义了time变量并启动计时;
第31~34行变量files文件列表,并挨个文件计算md5码,将得到的md5码保存到列表md5_a;
第35~36行,统计计算用时并输出;

为了对两种方法的计算结果进行对比,确保两种方法计算的到的md5码一致,我们将得到的md5码列表保存到文件,先保存单线程方案的结果:
代码清单13-03-03main.cppint main(int argc, char * argv[])
{
      //......
       // 将结果a写入文件
       {
          QString strFileName;
          strFileName =ns_train::getPath("$TRAINDEVHOME/test/chapter13/ks13_03/md5_a.txt");
          QString strDir =ns_train::getDirectory(strFileName);
          QDir dir;
          dir.mkpath(strDir);
   
          QFile file(strFileName);
          // 打开方式:只读、文本方式
          if (!file.open(QFile::Truncate |
                         QFile::WriteOnly |
                         QFile::Text)) {
            qDebug("open failed! filename is:%s",
                     strFileName.toLocal8Bit().data());            
          }
          else {
            QList<QByteArray>::iteratoriteMd5 = md5_a.begin();
            for (; iteMd5 != md5_a.end();iteMd5++) {
                  file.write(*iteMd5);
            }
          }
          file.close();
}
// ......
}

    代码清单13-03-03实现的功能是将md5_a中的md5码保存到文件:
    "$TRAINDEVHOME/test/chapter13/ks13_03/md5_a.txt"

然后,我们看一下使用QtCocurrent的实现方案
       代码清单13-03-04main.cpp
#include <QApplication>
#include <QTranslator>// 国际化
#include "qglobal.h"
#include <QLibraryInfo> // 国际化
   
#include <QDir>
#include <QFile>
#include <qtconcurrentmap.h>
#include <iostream>
#include <functional>
#include "baseapi.h"
using std::cout;
using std::endl;
   
int main(int argc, char * argv[])
{
      //......
       // 利用std::function声明的函数
       std::function<QByteArray(const QString&)> getMd5OfFile = [](const QString &strFileName)-> QByteArray
       {
          qDebug() << "get md5 inthread" << QThread::currentThread();
          return ns_train::getMd5(strFileName);
       };
   
       // Use QtConcurrentBlocking::mapped to apply the getMd5OfFile functionto all the
       // files in the list.
       QList<QByteArray> md5_b;
       int mapTime = 0; // 使用QtConcurrent的map计算md5码所需的时间。
       {
          QTime time;
          time.start();
          md5_b =QtConcurrent::blockingMapped(files, getMd5OfFile);
          mapTime = time.elapsed(); // ms
          qDebug() << "Map:"<< mapTime;
   
       }
       qDebug() << "Map speedup x" <<((double)singleThreadTime - (double)mapTime) / (double)mapTime + 1;
// ......
}

       代码清单13-03-04中:
       第19~24行,我们利用std::function包装了一个lambda表达式(C++11特性)。其实是定义了一个函数:函数名为:getMd5OfFile,函数提供 一个const QString&类型的参数,返回值为QByteArray类型。等号后面是Lambda表达式的语法。Lambda函数可以引用在它之外声明的变量. 这些变量的集合叫做一个闭包. 闭包被定义在 Lambda表达式声明中的方括号 [] 内。[]后面是参数列表(关于Lambda表达式我们不做过多讲解,大家先简单把它理解为一个函数吧)。该函数的实现中,第22行打印了当前线程id,第23行是函数的核心代码:通过调用ns_train::getMd5()计算指定文件的md5码。但是这里的Lambda表达式仅仅定义了函数,具体的函数调用在后面。
       第29~38行是真正的函数调用。第32、33行启动计时器以便计算该方案的耗时,第35行计算耗时;第36行输出耗时;第34行是真正的函数调用,通过QtConcurrent::blockingMapped()调用实现对前面Lambda表达式的执行。我们单独看一下这一行:
代码清单13-03-05main.cpp
md5_b =QtConcurrent::blockingMapped(files, getMd5OfFile);
    在代码清单13-03-05中,第一个参数传入了文件名列表,第二个参数是需要被调用的函数名,返回值存放在md5_b中,该变量在29行定义。



代码清单13-03-06main.cpp
// 将结果b写入文件
int main(int argc, char * argv[])
{
      //......
      qDebug() <<"Map speedup x" << ((double)singleThreadTime -(double)mapTime) / (double)mapTime + 1;
       // 将结果b写入文件
       {
          QString strFileName;
          strFileName =ns_train::getPath("$TRAINDEVHOME/test/chapter13/ks13_03/md5_b.txt");
          QString strDir =ns_train::getDirectory(strFileName);
          QDir dir;
          dir.mkpath(strDir);
   
          QFile file(strFileName);
          // 打开方式:只读、文本方式
          if (!file.open(QFile::Truncate |QFile::WriteOnly | QFile::Text)) {
            qDebug("open failed! filename is:%s",
                  strFileName.toLocal8Bit().data());
          }
          else {
            QList<QByteArray>::iteratoriteMd5 = md5_b.begin();
               for (; iteMd5 !=md5_b.end(); iteMd5++) {
                  file.write(*iteMd5);
            }
          }
          file.close();
}
}
    在代码清单13-03-06中,跟方案1一样,将方案2结果b保存到文件中。在第5~6行将两个方案的用时进行比较并计算出比例值并输出,以便比较这两个方案哪个用时更少。读者可以自行运行一下本示例附件源码,看看效果,如果不明显可以试试多准备些文件。

结语
------------------------------------------------------------------------------------
       本文介绍了使用QtConcurrent处理并发操作的一种应用,关键在于代码清单13-03-07这一行:
代码清单13-03-07main.cpp
md5_b = QtConcurrent::blockingMapped(files,getMd5OfFile);
读者可以尝试把本示例的用法应用到类似的不同场景中。




页: [1]
查看完整版本: KS13-03 使用QtConcurrent处理并发-Map