【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
在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计时并不准确,但是这里已经可以看出问题了。
提高了将近三倍的速度。
这里分为三个部分来谈Concurrent在Qt中的用法(QThread在这里就不谈了,有点low-level,灵活性虽强,但不好控制,需要较好的多线程经验)。
1. QtConcurrent::run()
QtConcurrent::run()使用Qt的全局线程池,找到一个空闲线程执行函数。在操作系统中,是有流水线技术的,那么IO操作与CPU运算是可以同时进行的,如果我们的操作中有这两种操作,就可以使用QtConcurrent并发完成。看下面的函数原型:
QFuture<T> QtConcurrent::run ( Function function, ... )
第一个是函数名,接下来是不定长的参数列表。每个线程就去执行这个函数,直到完成任务线程归还给线程池。这里就有一个问题,假如我现在有很多任务,那么,需要多少个线程来做这些任务呢?当然,并不是开辟的线程越多越好的,开辟过多的线程不仅浪费资源,同时也会使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,QGenericReturnArgument ret, QGenericArgument val0 = 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高级编程》中的示意图:
这里有一个地方要注意:在filtered()之前,我们就应该将watcher的信号链接到相应的槽上去。下面是具体用法(以filtered为例):
函数原型
QFuture<T> QtConcurrent::filtered ( const Sequence & sequence, FilterFunction filterFunction );
QFuture<T> QtConcurrent::filtered ( ConstIterator begin, 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抠出来用。
来源:oschina
链接:https://my.oschina.net/u/247612/blog/67836