1、概 述
并发是指在同一时间段内,能够处理多个任务的能力。为了提升应用的响应速度与帧率,以及防止耗时任务对主线程的干扰,HarmonyOS系统提供了异步并发和多线程并发两种处理策略。
-
异步并发:指异步代码在执行到一定程度后会被暂停,以便在未来某个时间点继续执行,这种情况下,同一时间只有一段代码在执行。
-
多线程并发:它允许在同一时间段内同时执行多段代码。在主线程继续响应用户操作和更新UI的同时,后台也能执行耗时操作,从而避免应用出现卡顿。
并发能力在多种场景中都有应用,其中包括单次I/O任务、CPU密集型任务、I/O密集型任务和同步任务等。我们可以根据不同的场景,选择相应的并发策略进行优化和开发。
ArkTS支持异步并发和多线程并发。
-
-
Promise和async/await提供异步并发能力,适用于单次I/O任务的开发场景。(之前我们已经讨论过,参看文章👉🏻 异步场景: promise、async函数与await命令介绍)
-
TaskPool和Worker提供多线程并发能力,适用于CPU密集型任务、I/O密集型任务和同步任务等并发场景。
-
目前ArkTs提供的多线程并发能力都基于Actor并发模型,在介绍TaskPool和Worker前,我们先了解下多线程并发中的两个并发模型:Actor并发模型、内存共享并发模型。
为了解释两个并发模型,后文以经典的生产者消费者问题为例,对比呈现这两种模型在解决具体问题时的差异。
2、Actor并发模型
Actor并发模型中,每一个线程都是一个独立Actor,每个Actor有自己独立的内存,Actor之间通过消息传递机制触发对方Actor的行为,不同Actor之间不能直接访问对方的内存空间。
⭐️ Actor并发模型特点:Actor模型不同角色之间并不共享内存,生产者线程和UI线程都有自己独占的内存。
在生产者消费者问题中,过程为:生产者生产出结果后通过序列化通信将结果发送给UI线程,UI线程消费结果后再发送新的生产任务给生产者线程。示意图如下:
以下示例简单展示了如何使用基于Actor模型的TaskPool并发能力来解决生产者消费者问题【重点关注4~8行,11~14行,35~39行】。
import { taskpool } from '@kit.ArkTS';
// 跨线程并发任务
@Concurrent
async function produce(): Promise<number>{
// 添加生产相关逻辑
console.log("producing...");
return Math.random();
}
class Consumer {
public consume(value : Object) {
// 添加消费相关逻辑
console.log("consuming value: " + value);
}
}
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
Button() {
Text("start")
}.onClick(() => {
let produceTask: taskpool.Task = new taskpool.Task(produce);
let consumer: Consumer = new Consumer();
for (let index: number = 0; index < 10; index++) {
// 执行生产异步并发任务
taskpool.execute(produceTask).then((res : Object) => {
consumer.consume(res);
}).catch((e : Error) => {
console.error(e.message);
})
}
})
.width('20%')
.height('20%')
}
.width('100%')
}
.height('100%')
}
}
3、内存共享并发模型
内存共享并发模型中,多个线程同时执行复数任务,这些线程依赖同一内存并且都有权限访问,线程访问内存前需要抢占并锁定内存的使用权,没有抢占到内存的线程需要等待其他线程释放使用权再执行。
⭐️ 内存共享并发模型特点:线程间共享内存,内存的操作有排他性。
为了避免不同生产者或消费者同时访问一块共享内存的容器时产生的脏读,脏写现象,同一时间只能有一个生产者或消费者访问该容器,也就是不同生产者和消费者争夺使用容器的锁。当一个线程获取锁之后其他线程需要等待该线程释放锁之后才能重新尝试获取锁以访问该容器。示意图如下:
以下示例伪代码和示意图展示了如何使用内存共享模型解决生产者消费者问题。
// 此段示例为伪代码仅作为逻辑示意,便于开发者理解使用内存共享模型和Actor模型的区别
BufferQueue {
Queue queue
Mutex mutex
add(value) {
// 尝试获取锁
if (mutex.lock()) {
queue.push(value)
mutex.unlock()
}
}
take() {
// 尝试获取锁
if (mutex.lock()) {
if (queue.empty()) {
return null
}
let res = queue.pop(value)
mutex.unlock()
return res
}
}
}
// 构造一段全局共享的内存
let g_bufferQueue = new BufferQueue()
Producer {
run() {
let value = random()
// 跨线程访问bufferQueue对象
g_bufferQueue.add(value)
}
}
Consumer {
run() {
// 跨线程访问bufferQueue对象
let res = g_bufferQueue.take()
if (res != null) {
// 添加消费逻辑
}
}
}
Main() {
let consumer = new Consumer()
let producer = new Producer()
// 多线程执行生产任务
for 0 in 10 :
let thread = new Thread()
thread.run(producer.run())
consumer.run()
}
Actor并发模型对比内存共享并发模型的优势在于不同线程间内存隔离,不会产生不同线程竞争同一内存资源的问题。开发者不需要考虑对内存上锁导致的一系列功能、性能问题,提升了开发效率。 |