阅读QtCreator--Concurrent预备知识

来源:互联网 发布:草图大师for mac破解 编辑:程序博客网 时间:2024/05/20 17:26

摘自:http://m.oschina.net/blog/67836

 

在QtCreator当中用到了不少的Concurrent(并发),比如编译时,搜索时等。其实在很多场合中都需要用到,一般是CPU去做一项大任务(花费较长时间)时相应用户操作。另一个重要用途就是在当前这个多核,甚至多CPU的年代,并行变成成为一种时尚了,它也确实提高了应用程序的性能。我的电脑是单CPU,2核心4线程,所以相比单应用程序,应该可以将性能提高将近4倍(当然不会是4倍的)。我所听过的有很多库是这方面的,比如CUDA,OpenCL,OpenMP。Qt是怎么做的还真不知道,望高手指教。首先来测试下:

#include <QtCore/QCoreApplication>#include <QtConcurrentRun>#include <QtConcurrentMap>#include <qmath.h>#include <QFuture>#include <QVector>#include <QTime>#include <QObject>#include <QFutureWatcher>#include "myslot.h"QTime t;void test(const int &N){    int k = N * 100;    int v = 0;    for(int i = 0; i < k; ++i)        ++v;}int main(int argc, char *argv[]){    QCoreApplication a(argc, argv);    MySlot ms;    QVector<int> datas(1000, 12343);    t.start();    for(int i = 0; i < 1000; ++i)    {        test(datas[i]);    }    printf("%d ms\n", t.elapsed());    t.restart();    QFutureWatcher<void> *pW = new QFutureWatcher<void>;    QObject::connect(pW, SIGNAL(finished()), &ms, SLOT(onFinish()));    QFuture<void> future = QtConcurrent::map(datas, test);    pW->setFuture(future);    return a.exec();}

这里的MySlot里面就有一个onFinish()而已,完成时间输出,虽然QTime计时并不准确,但是这里已经可以看出问题了。

image

提高了将近三倍的速度。

这里分为三个部分来谈Concurrent在Qt中的用法(QThread在这里就不谈了,有点low-level,灵活性虽强,但不好控制,需要较好的多线程经验)。

1. QtConcurrent::run()

QtConcurrent::run()使用Qt的全局线程池,找到一个空闲线程执行函数。在操作系统中,是有流水线技术的,那么IO操作与CPU运算是可以同时进行的,如果我们的操作中有这两种操作,就可以使用QtConcurrent并发完成。看下面的函数原型:

QFuture<T> QtConcurrent::run ( Functionfunction, ... )

第一个是函数名,接下来是不定长的参数列表。每个线程就去执行这个函数,直到完成任务线程归还给线程池。这里就有一个问题,假如我现在有很多任务,那么,需要多少个线程来做这些任务呢?当然,并不是开辟的线程越多越好的,开辟过多的线程不仅浪费资源,同时也会使CPU因切换时间片而性能下降。为了减少线程数量,我可以将任务分成N组,这个N的取值可以根据具体的计算机的CPU来确定,我的是4核心,可以使用4个线程4个分组(这个不是最好的,因为没有考虑到IO与CPU并发的问题)。Qt中有一个函数可以返回一个较合适的CPU线程数量QThread::idealThreadCount(),他返回的就是CPU的核心数量。
如果希望在每个组里根据当前处理状态来通知消息,可以使用QApplication::postEvent(), QApplication :: sendEvent()或者QMetaObject::InvokeMethod(),还有signal-slot,他们各有各自的好处。

postEvent()类似与windows中的PostMessage(),使用非阻塞的方式发送消息到消息队列当中,相反SendMessage()就是阻塞的方式了,知道消息被处理之后才会返回。但是Qt中的sendEvent()有些特别,如果在别的线程sendEvent(),你会得到这样的ASSERT:
ASSERT failure in QCoreApplication::sendEvent: "Cannot send events to objects owned by a different thread. Current thread c33f640. Receiver '' (of type 'Widget') was created in thread 277818"。
在Qt高级编程中有这样的一些话,“sendEvent()方法立刻分派消息--但是应该小心使用,或者就不使用,比如在多线程编程中使用sendEvent()会使事件句柄归发送者线程拥有,而不是接收者线程。并且没有事件压缩或者重排序,sendEvent()不会删除事件,因此应该在栈上创建事件。”
所以还是用postEvent比较好,具体用法参考Qt Assistant。

invokeMethod()的函数原型非常的长,如下

bool QMetaObject::invokeMethod ( QObject * obj, const char * member, Qt::ConnectionType type,QGenericReturnArgumentret, QGenericArgumentval0 = QGenericArgument( 0 ), QGenericArgument val1 = QGenericArgument(), QGenericArgument val2 = QGenericArgument(), QGenericArgument val3 = QGenericArgument(), QGenericArgument val4 = QGenericArgument(), QGenericArgument val5 = QGenericArgument(), QGenericArgument val6 = QGenericArgument(), QGenericArgument val7 = QGenericArgument(), QGenericArgument val8 = QGenericArgument(), QGenericArgument val9 = QGenericArgument() ) [static]。
也就是说invokeMethod最多支持10个参数,该函数还有不少的重载。具体使用方法如下:

 QString retVal; QMetaObject::invokeMethod(obj, "compute", Qt::DirectConnection,                           Q_RETURN_ARG(QString, retVal),                           Q_ARG(QString, "sqrt"),                           Q_ARG(int, 42),                           Q_ARG(double, 9.7));

第三个参数是链接类型,在多线程中Qt::QueuedConnection是经常使用的。Q_RETURN_ARG只有在使用Qt::DirectConnection才会有效。

 

2. 使用QRunnable

使用QRunnable就需要派生一个子类了,然后重写虚函数run()。在run函数中完成要做的任务,但是QRunnable不是QObject的子类,那么只能用invoke方法了和自定义事件(custom event)。另外在run结束之后,线程池将会删除这个runnable对象,那么在构造函数里使用setAutoDelete(false)可以将控制权转交给用户。创建任务时,使用的方法是QThreadPool::globalInstance()->start(MyRunnableObj);

使用QtConcurrent::run()方法可以返回一个QFuture<T>, 我们可以使用QFutureWatcher<T>来跟踪处理的过程,而使用QRunnable类就需要我们自己管理了。

3. 使用QtConcurrent监视进度

有四种方式来监视任务进度,filter,mapper,reducer,function。最后一种是无法跟踪具体进度的,只能监视到任务开始与任务结束。这里分别说一下前三者的用法。
给定一个集合如QList,QVector等,通过一个过滤filter函数的返回值返回一个新的集合;mapper返回新类型的集合,reducer就是将集合中的值merge成一个值(比如求和等,这个值也可以是一个新的集合,那么所谓的值就是集合)。QtConcurrent::run()等方法都会返回一个QFuture<T>类型的对象,直接访问该对象将会阻塞,那么就要使用非阻塞的QFutureWatcher<T>来监视,它所监视的事件有paused, resumed, canceled。其中QFuture<T>中的T需要是filter等操作的结果类型,QFutureWatcher<T>就需要和他监视的QFuture的类型相同才对。来看一下《Qt高级编程》中的示意图:

image

这里有一个地方要注意:在filtered()之前,我们就应该将watcher的信号链接到相应的槽上去。下面是具体用法(以filtered为例):

函数原型

QFuture<T> QtConcurrent::filtered ( const Sequence &sequence, FilterFunction filterFunction );
QFuture<T> QtConcurrent::filtered ( ConstIteratorbegin, ConstIterator end, FilterFunction filterFunction )

说明前边的参数可以是一个集合,或者是两个const的迭代器,最后一个参数就是相应的过滤函数。过滤函数的圆形需要是这样的:

bool function(const T &t);

用法如下(来自Qt Assistant):

bool allLowerCase(const QString &string) {     return string.lowered() == string; } QStringList strings = ...; QFuture<QString> lowerCaseStrings = QtConcurrent::filtered(strings, allLowerCase);

 

这里学会了一种叫函数对象的东东,上一段代码先看,其实就是重载()操作符,另外typedef 该操作符函数的返回值为result_type。

    
struct StartsWith {     StartsWith(const QString &string)     : m_string(string) { }     typedef bool result_type;     bool operator()(const QString &testString)     {         return testString.startsWith(m_string);     }     QString m_string; }; QList<QString> strings = ...; QFuture<QString> fooString = QtConcurrent::filtered(images, StartsWith(QLatin1String("Foo")));

好,这部分就做这么多记录,然后准备把QtCreator的ProgressManager抠出来用。

0 0