TaskPool和Worker的对比实践
概述
ArkTS提供了TaskPool与Worker两种多线程并发方案,下面我们将从其工作原理、使用效果对比两种方案的差异,进而选择适用于ArkTS图片编辑场景的并发方案。
TaskPool和Worker工作原理
TaskPool与Worker两种多线程并发能力均是基于 Actor并发模型实现的。Worker主、子线程通过收发消息进行通信;TaskPool基于Worker做了更多场景化的功能封装,例如支持任务组TaskGroup、任务优先级设置、取消任务等功能,且可以根据任务数量进行自动的扩容与缩容,还可以根据任务优先级进行任务调度。
Worker工作原理
Worker拥有独立的运行环境,每个Worker线程和主线程一样拥有自己的内存空间、消息队列(MessageQueue)、事件轮询机制(EventLoop)、调用栈(CallStack)等。线程之间通过Message进行交互,如下图所示:
在多核的情况下(下图中的CPU 1和CPU 2同时工作),多个Worker线程(下图中的Worker thread1和Worker thread2)可以同时执行,因此Worker线程做到了真正的并发,如下图所示:
TaskPool工作原理
TaskPool在Worker之上实现了调度器和Worker线程池。在主线程(ArkTS Main Thread)中调用execute接口会将待执行的任务方法及参数信息,根据设置的任务优先级放入任务队列(TaskQueue)中等待调度执行。调度器会依据调度算法(优先级,防饥饿),从优先级队列中取出任务进行序列化,放入TaskPool中的Worker线程池,工作线程(ArkTS Worker Thread)根据调度器的安排执行任务方法,并将任务处理结果进行反序列化,最终以Promise-Then的方式返回给主线程。TaskPool的工作线程池会根据待执行的任务数量,任务的执行时间进行相应的扩容与缩容。原理图如下所示:
TaskPool与Worker并发方案对比
使用场景
本章节主要介绍Worker与TaskPool并发方案在ArkTS图片编辑场景下的使用及性能差异。分别从编码效率、线程创建耗时、数据传输、任务执行耗时、应用运行内存占用几个维度进行分析,对比不同方案各自的优缺点,以供开发者在遇到不同场景时参考。
编码效率对比
使用Worker处理图片
使用Worker并发处理图片时需要开发者根据任务量的多少,控制Worker实例运行的数量,最多可以同时运行64个实例。为了避免产生大量的线程创建开销,需要开发者尽量复用已创建线程处理耗时任务,任务执行完成时需要及时销毁Worker,以免线程资源长期被占用影响其他任务的执行。
使用Worker进行图片处理分以下步骤:
- 根据任务数创建Worker实例,由于Worker最多同时运行的子线程数量为64个(API12新增支持,旧版本为8个),所以当任务数超过64时需要做相应限制。
- 根据任务数将图片像素字节数进行拆分,并分配给已创建的Worker实例进行计算处理。
- 接收到任务的Worker子线程会进行像素计算,并将计算结果返回给主线程。
- 当主线程接收到子线程的计算结果时,如果还有剩余任务没有处理,就会复用该子Worker线程继续处理剩余任务;当所有任务都处理完成时,销毁所有子线程,并将所有任务处理结果进行合并进而更新UI。
可以发现使用Worker需要关注任务池个数上限,并管理Worker线程的生命周期,当任务数较多时难免会增加代码的复杂度。
使用TaskPool处理图片
TaskPool提供了比较简洁的API接口,开发者只需把任务方法、参数传入execute接口,等待任务执行完成返回结果就行了,无需关注线程的创建,系统会自动根据任务量多少进行扩容及缩容。TaskPool还提供了一些常用功能,支持任务组TaskGroup、配置任务优先级、任务取消,以满足开发者更多的开发场景。本实践利用TaskGroup任务组的能力,将一个大的任务拆分成多个小的任务放进一个任务组中等待调度执行。
使用TaskPool进行图片处理步骤如下,其中根据任务数对图片数据进行拆分、图片像素点的计算,以及任务结果的合并与Worker的处理逻辑一致,在此不再赘述。
- 根据任务数拆分任务,并把任务放进任务组里面。
- 调用TaskPool的execute接口将TaskGroup任务组中的每个任务放入线程池中,系统会根据第二个参数任务优先级进行调度执行。TaskGroup任务组内的每个任务的执行顺序会与执行结果数组中的顺序保持一致。将任务组的执行结果(数组内多个任务的处理结果)合并并更新UI。
使用TaskPool并发方案处理耗时任务代码写法比较简洁,开发者更容易上手。TaskPool支持任务组、任务优先级、取消任务等能力,为开发者提供了更多场景的的选择。
根据以上示例代码对比可以看出,使用Worker需要开发者关注线程数量的上限,管理线程生命周期,随着任务的增多也会增加线程管理的复杂度。使用TaskPool并发方案处理耗时任务代码写法比Worker简洁,开发者很容易上手。TaskPool支持任务组、任务优先级、取消任务等能力,为开发者提供了更多场景的的选择。
数据传输
使用Worker与TaskPool处理并发任务时需要将数据从主线程传递到任务池的执行线程。目前支持传输的数据对象可以分为普通对象、可转移对象、可共享对象、Native绑定对象、Sendable对象五种,具体可参考指南文档。Worker与TaskPool均提供了两种传递数据的方式。
- 转移控制权:可以将transfer列表中的ArrayBuffer对象在传输时转移控制权至工作线程,而非复制内容到工作线程。传输后当前的ArrayBuffer失效,在宿主线程中将变为不可用,不允许再访问。
- 深拷贝:将宿主线程的数据复制一份传递给执行线程,执行线程对数据的修改不会对宿主线程中的原数据产生影响。
其中Worker提供postMessage接口,TaskPool提供了setTransferList接口,开发者可以根据实际需要,调整参数控制采用哪种方式传递数据。
TaskPool与Worker底层都是采用了同一套序列化与反序列化的机制。主要差异体现在TaskPool支持任务方法的传递,而Worker的任务方法需要写在对应的Worker.js文件中,相较于Worker,TaskPool多了任务方法的序列化与反序列化步骤。我们以TaskPool在任务数为1时(任务方法、参数、运行结果)的序列化与反序列化为例,统计一下序列化与反序列化的相关数据如下表所示:
\ | 序列化数据量(bytes) | 序列化时间(μs) | 序列化效率(B/μs) | 反序列化时间(μs) | 反序列化效率(B/μs) |
---|---|---|---|---|---|
方法 | 58 | 9.549 | 6.833 | 43.749 | 1.457 |
参数 | 217 | 36.111 | 6.023 | 115.294 | 1.933 |
结果 | 47 | 16.667 | 2.990 | 85.243 | 0.567 |
TaskPool与Worker都具有转移控制权、深拷贝两种方式,Worker不支持任务方法的传递,只能将任务方法写在Worker.js文件中。TaskPool支持任务方法的传递,因此相较于Worker,TaskPool多了任务方法的序列化与反序列化步骤。数据传输两者差异不大。
任务执行完成耗时对比
分别在中载、重载环境下运行,随着任务数的增多,图片编辑完成任务耗时,如下图所示:
经过以上中载、重载环境下的对比实验可以发现,并发可以带来约50%~65%收益,但并不是任务数越多越好,需要开发者根据任务及计算情况自己控制;随着任务数的增多在重载环境下TaskPool与Worker耗时差异比在中载环境下大,这是由于TaskPool支持高优先级设置,在系统资源不足时,高优先级的任务更容易获得系统资源,所以TaskPool执行耗时任务相对Worker稍快一些;中载环境下由于系统资源充足,TaskPool的高优先设置效果没有那么明显,所以TaskPool与Worker完成任务耗时几乎相当。
运行时内存占用对比
分别在中载、重载环境下,随着任务数的增多,统计图片编辑前一刻与完成任务时刻应用内存增量的变化情况,如下图所示:
从以上实验数据可以看出:
任务数较少时使用Worker与TaskPool的运行内存差别不大,随着任务数的增多TaskPool的运行内存明显比Worker大。这是由于TaskPool在Worker之上做了更多场景化封装,TaskPool实现了调度器和Worker线程池,随着任务数的增多,运行时会多占用一些内存空间,待任务执行完毕之后都会进行回收和释放。
总结
对比维度 | Worker | TaskPool |
---|---|---|
编码效率 | Worker需要开发者关注线程数量的上限,管理线程生命周期,随着任务的增多也会增加线程管理的复杂度。 | TaskPool简单易用,开发者很容易上手。 |
数据传输 | TaskPool与Worker都具有转移控制权、深拷贝两种方式,Worker不支持任务方法的传递,只能将任务方法写在Worker.js文件中。 | 传输方式与Worker相同;TaskPool支持任务方法的传递,因此相较于Worker,TaskPool多了任务方法的序列化与反序列化步骤。数据传输两者差异不大。 |
任务执行耗时 | 任务数较少时优于TaskPool,当任务数大于8后逐渐落后于TaskPool。 | 任务数较少时劣于Worker,随着任务数的增多,TaskPool的高优先级任务模式能够更容易的抢占到系统资源,因此完成任务耗时比Worker少。 |
运行时内存占用 | 运行时占用内存较少。 | 随着任务数的增多占用内存比Worker高。 |