目录
前言
一、简介
1.1 并发执行机制
1.2 多线程机制
1.3 多线程处理的方式
二、Qt高级线程类实现多线程
2.1 在线程中执行函数
2.1.1 使用QtConcurrent::run()
2.1.2 使用QRunnable
2.1.3 示例代码
2.2 线程中的过滤和映射
2.2.1 过滤器的使用
2.2.2 映射器的使用
2.2.3 简化器的使用
2.2.4 示例代码
三、QThread类实现多线程
3.1 独立项的处理
3.2 共享项的处理
总结
前言
周末在图书馆不经意间翻阅了《Qt高级编程》后就爱释手,作者Mark Summerfield高瞻远瞩、思路清晰,刚好满足我提升Qt知识的需要,于是立马从图书馆借出,舔了三月。
一、简介
1.1 并发执行机制
一个处理器划分为若干个短的时间片,每个时间片依次轮流地执行处理各个应用程序,由于一个时间片很短,相对于一个应用程序来说,就好像是处理器在为自己单独服务一样,从而达到多个应用程序在同时进行的效果。
1.2 多线程机制
多线程就是把操作系统中的这种并发执行机制原理运用在一个程序中,把一个程序划分为若干个子任务,多个子任务并发执行,每一个任务就是一个线程。
对比单线程来分析,多线程的优缺点如下:
优点:
- 通过将耗时长的任务放入辅助线程运行来避免冻结用户界面;
- 程序的运行速度可能加快;
- 任务执行能随时中断;
- 可设置各个任务的优先级;
缺点:
- 占用和消耗更多的内存空间;
- 增加了程序的调试复杂度;
- 增加了程序的操作风险度,容易发生死锁而导致程序崩溃;
1.3 多线程处理的方式
可以把处理的数据分配给一些线程来执行,有三种方式:
- 利用QtConcurrent::run()在Qt全局线程池的辅助线程中运行函数来处理;
- 创建QRunnable对象并在Qt全局线程池辅助线程来处理;
- 创建QThread对象并把它作为辅助线程来处理;
二、Qt高级线程类实现多线程
2.1 在线程中执行函数
2.1.1 使用QtConcurrent::run()
QtConcurrent 是命名空间 ,它提供了高层次的函数接口 (APIs),使所写程序,可根据计算机的 CPU 核数,自动调整运行的线程数目。QtConcurrent::run()函数的参数包含一个函数、一个或多个传递给函数的可选参数,它会在Qt全局线程池中的一个辅助线程中执行该函数,函数如下:
typedef int Function;
namespace QtConcurrent {
template <typename T>
QFuture<T> run(Function function, ...);
template <typename T>
QFuture<T> run(QThreadPool *pool, Function function, ...);
} // namespace QtConcurrent
函数返回T型对象,Function是一个指向仿函数的指针,省略号代表一个或多个变量参数,当函数被QtConcurrent调用时,会把它们传递给Function,且参数必须与Function的形式相匹配。
由于QtConcurrent不继承QObject,所以不支持信号槽机制。它有个明显的缺点:没有提供进度通知功能和结束通知功能。
解决函数在辅助线程中的进度通知,可以使用自定义事件,实现的方案如下:
- 定义一个自定义事件;
- 主窗口中重载event()方法;
- 线程函数中使用QApplication::postEvent()方法发送自定义事件。
解决函数在辅助线程中的结束通知,有两种方式:
- 使用QTimer::singleShot()轮询结果;
- 使用QFutureWatcher<T>查询辅助线程状态。
2.1.2 使用QRunnable
QRunnable类在Qt中是全部可运行对象的基类,QRunnable类如下:
class Q_CORE_EXPORT QRunnable
{
int ref;
friend class QThreadPool;
friend class QThreadPoolPrivate;
friend class QThreadPoolThread;
public:
virtual void run() = 0;
QRunnable() : ref(0) { }
virtual ~QRunnable();
bool autoDelete() const { return ref != -1; }
void setAutoDelete(bool _autoDelete) { ref = _autoDelete ? 0 : -1; }
};
QRunnable类的run()函数表示一个任务,任务被分配在Qt 全局线程池中的一个线程中。
因为QRunnable不继承QObject,所以不内置支持信号槽机制,要实现进度通知功能则可以采用自定义事件或调用主窗口的槽函数来实现。自定义事件的使用在上一章节已经介绍,此处不重复讲解,使用主窗口槽函数的方法如下:
- 主窗口中定义一个槽函数,用于进度显示;
- 实现一个继承QRunnable类的任务对象,
- 任务对象支持通过主窗口指针调用槽函数来实现跨线程通信;
- 调用QThreadPool::start()方法来启动辅助线程执行任务;
2.1.3 示例代码
使用QtConcurrent::run()和QRunnable类实现在线程中执行函数的示例代码,可从链接下载:
https://pan.baidu.com/s/1yuOfHZSb4a6XsKRZ2vOC-g 提取码:sk45
2.2 线程中的过滤和映射
QtConcurrent提供四种函数:运行函数、过滤器、映射器和简化器。
当有大量数据项需要处理,如果为每个项都创建一个线程将会导致大量的系统开销,因此,我们可以采取依次处理数据,创建少量的辅助线程并让每个线程只处理一组项。这就是过滤器、映射器和简化器的使用场景。
QtConcurrent的过滤器和映射器遵守函数式编程方法。
2.2.1 过滤器的使用
过滤器带一个数据项的序列,过滤器函数标记符合条件的数据项且将它们输出到一个新的数据项序列,从概念上来分析,它的工作方式表示如下:
// Filter
QList<Item> filter<QList<Item> items, FilterFunction isOK)
{
QList<Item> results;
foreach (Item item, items)
{
if (isOK(item))
{
results << item;
}
}
return results;
}
2.2.2 映射器的使用
映射器带一个序列和一个映射函数,通过对原始数据项序列进行映射,并产生一个新序列且新序列的项数与原始序列相同。从概念上来分析,它的工作方式表示如下:
// Mapper
QList<TypeNew> filter<QList<Type> items, MapFunction mapFun)
{
QList<TypeNew> results;
foreach (Type item, items)
{
results << mapFun(item);
}
return results;
}
2.2.3 简化器的使用
简化器带一个项序列,将序列处理或汇总成单一的项。可以将过滤与简化结合,或映射与简化结合。从概念上来分析,它的工作方式表示如下:
// Custom structure
struct ResultType
{
ResultType()
{
sum = 0;
}
void mergeFun(int item)
{
sum += item;
}
int sum;
};
// Filter-Reduce
ResultType filter_reduce<QList<Item> items, FilterFunction isOK)
{
ResultType result;
foreach (Item item, items)
{
if (isOK(item))
{
result.MergeFun(item)
}
}
return result;
}
// Map-Reduce
ResultType filter_reduce<QList<Item> items, MapFunction mapFun)
{
ResultType result;
foreach (Item item, items)
{
result.mergeFun(mapFun(item))
}
return result;
}
2.2.4 示例代码
使用QtConcurrent::run()和QRunnable类实现过滤器、映射器和简化器的示例代码,参考链接:
https://pan.baidu.com/s/1OoIXDuhtAgdaMhKx6Alcwg 提取码:7ubf
三、QThread类实现多线程
当少数项需要后台处理时,可以使用QThread来追踪进度和进度完成情况。QThread继承QObject类和支持Qt的信号和槽机制,我们可以用子类重新实现run()方法,并通过调用start()方法来启动线程。根据处理项的访问特征,可分成独立项处理和共享项处理两种方法。
3.1 独立项的处理
如果需要处理的项较少,且所有的处理项都是独立的,可以使用使用独立项的处理方法。每个处理项使用一个单独的QThread子类实例来进行处理。所有处理过程独立完成,不需要锁。
针对独立项的处理,作者编写了"Cross Fader”应用程序,此程序让用户选择两幅图像,然后通过建立一个用户定义的中间图像数量来生成一个平滑过滤的图像序列。
输入开始图像和结束图像,使用4个独立线程来生成4幅平滑过渡中间图,效果如下:
详细的QThread类独立项处理方法可参考crossfader示例源码,下载地址如下:
链接:https://pan.baidu.com/s/1tLTZU_bjkvZYzNAEHtGUdw 提取码:jpao
3.2 共享项的处理
如果需要处理的项比较多,而且不知道项的大小,使用共享的工作序列能较好的处理这种情况。先将工作任务放到序列中,并启动一定数量的辅助线程先来开始数据处理,然后再逐步添加其它工作任务。每个工作序列内置锁定且是线程安全的,每个访问序列的辅助线程将其看作是特定类型的数据结构。
针对共享项的处理,作者编写了"Find Duplicates”应用程序,此应用程序在用户给定的目录和其子目录内查找重复的文件,通过分析每个文件的大小和MD5签名来确定文件是否重复。此应用程序的运行效果图如下:
本例的实现思路如下:
在用户选择的目录中创建一个子目录列表,将这个列表尽可能平均分配到辅助线程中,每一个辅助线程都会向一个共享的哈希表添加条目,这些条目是它所处理的每个目录中的每个文件及每个子目录中的文件,为了确保工作公平分配,创建一个单独的数据结构,它包含所有的文件并根据文件的尺寸和文件名分配到线程,并发执行线程后并生成结果。
详细的QThread类共享项处理方法可参考findduplicates示例源码,下载地址如下:
链接:https://pan.baidu.com/s/1myD_2SxIEPUU2PbfaeUHhw 提取码:55k1
总结
本书的多线程的示例源码基本上过了一遍,但其中的精髓还没有完全领悟,编写拥有多线程处理功能的程序比单线程要难度大很多,且有出现死锁的风险,务必小心谨慎。