鸿蒙开发中的并发处理

并发

  • 并发是指在一个时间段内,多个事件、任务或操作同时进行或者交替进行的方式。
  • 在计算机科学中,特指多个任务或程序同时执行的能力。
  • 并发可以提升系统的吞吐量、响应速度和资源利用率,并能更好地处理多用户、多线程和分布式的场景。
  • 常见的并发模型有多线程、多进程、多任务、协程等。

一、并发概述

为了提升应用的响应速度与帧率,避免耗时任务对主线程的影响,HarmonyOS提供了异步并发和多线程并发两种处理策略。

HarmonyOS中的异步并发和多线程并发

二、异步并发

  • Promise和async/await提供异步并发能力,是标准的JS异步语法。
  • 异步代码会被挂起并在之后继续执行,同一时间只有一段代码执行,适用于单次I/O任务的场景开发,例如一次网络请求、一次文件读写等操作。无需另外启动线程执行。
  • 异步语法是一种编程语言的特性,允许程序在执行某些操作时不必等待其完成,而是可以继续执行其他操作。

1、Promise

  • Promise是一种用于处理异步操作的对象。它表示一个可能还未完成的操作,并提供了一系列方法来处理操作的结果或错误。
  • Promise对象有三种状态:pending(进行中)、fulfilled(已完成)和rejected(已失败)。当操作完成时,Promise对象将会从pending状态转变为fulfilled或rejected状态,并调用相应的回调函数。
  • 使用Promise可以更加方便地管理异步操作,并避免回调函数嵌套过多的问题。
  • Promise是一种用于处理异步操作的对象。它可以认为是一个代理,用来代表一个尚未完成但最终会完成的操作。
Promise实例

 myAsyncFunction(): Promise<string> {  
      return new Promise((resolve, reject) => {  
        setTimeout(() => {  
          const success = true; // 模拟提交成功  
          if (success) {  
            resolve('提交成功');  
          } else {  
            reject('提交失败');  
          }  
        }, 1000)  
      })  
    }
import { BusinessError } from '@kit.BasicServicesKit';

this.myAsyncFunction().then((result: string) => {  
  console.log(result)  
})  
  .catch((error: BusinessError) => {  
    console.log(error.message)  
  })  
  .finally(() => {  
    console.log("操作完成")  
  })

通过then方法可以注册成功回调函数,通过catch方法可以注册失败回调函数,通过finally方法可以注册最终回调函数。

当异步操作完成后,Promise会根据操作的结果调用相应的回调函数。

async/await
  • async/await是一种用于处理异步操作的Promise语法糖
  • 基于Promise对象以一种更简单、易读的方式编写和处理异步代码

下面看看async/await的定义和使用

  • async关键字修饰的函数表示这是一个异步函数,会自动返回一个Promise对象
 async foo() {  
      // 异步操作  
      return "result"  
    }
await关键字
  • await关键字需要在async函数内部使用,等待一个Promise对象的解析结果,即Promise对象状态变为resolved(成功)或rejected(失败)
 async myAsyncFunction() : Promise<string> {  
      const result: string = await new Promise((resolve) => {  
        setTimeout(() => {  
          const success = true;   
    if (success) {  
            resolve('Hello, world!');  
          }  
        }, 3000)  
      })  
      console.log(result)  
      return result  
    }

    Text(this.message)  
      .id('Submit')  
      .fontSize(50)  
      .fontWeight(FontWeight.Bold)  
      .onClick(() => {  
        let res = this.myAsyncFunction().then((resolve => {  
          console.info("resolve is: " + resolve);  
        })).catch((error: BusinessError) => {  
          console.info("error is: " + error.message);  
        });  
        console.info("result is: " + res);  
      })

在async函数中使用await关键字可以实现类似同步代码的连续执行效果,而不需要嵌套使用回调函数或链式调用then方法。

async/await的优点
  • 代码可读性更高,更接近同步代码的写法,易于理解和维护
  • 可以在代码中使用try/catch语句来捕获和处理异步操作产生的错误
  • 可以使用常规的控制流语法(如循环、条件语句)来组织和管理异步代码的执行顺序
  • async/await是依赖Promise对象来处理异步操作
  • async/await只是一种更加简洁和易读的语法,本质上仍然是基于Promise的异步编程模式
IO异步任务开发示例
import fs from '@ohos.file.fs';  
    import common from '@ohos.app.ability.common';

    async write(data: string, file: fs.File): Promise<void> {  
      fs.write(file.fd, data).then((writeLen: number) => {  
        console.log("write data length is: " + writeLen)  
      }).catch((error: BusinessError) => {  
        console.error(`write data failed. Code is ${error.code}, message is ${error.message}`);  
      })  
    }  

    async testWriteFile() : Promise<void> {  
      let context = getContext() as common.UIAbilityContext  
      let filePath: string = context.filesDir + "/logFile.txt"  
      let file: fs.File = await fs.open(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)  
      this.write("Hello World", file).then(()=> {  
        console.log("write success")  
      }).catch((error: BusinessError) => {  
        console.error(`write data failed. Code is ${error.code}, message is ${error.message}`);  
      })  
    }

三、多线程并发

Actor并发模型

  • Actor并发模型是一种用于并发计算的编程模型
  • 在该模型中,每一个线程都是一个独立Actor,每个Actor有自己独立的内存,Actor之间通过消息传递机制触发对方Actor的行为
  • Actor并发模型对比内存共享并发模型的优势在于不同线程间内存隔离,不会产生不同线程竞争同一内存资源的问题
  • 不需要考虑对内存上锁导致的一系列功能、性能问题,提升了开发效率
  • ArkTS语言选择的并发模型就是Actor
  • ArkTS提供了TaskPool和Worker两种并发能力,TaskPool和Worker都基于Actor并发模型实现
TaskPool和Worker的实现特点对比

TaskPool和Worker的适用场景对比

性能方面使用TaskPool会优于Worker,因此大多数场景推荐使用TaskPool。 TaskPool偏向独立任务维度,任务在线程中执行,不需要关注线程的生命周期。超长任务(大于3分钟)会被系统自动回收。

适用场景:

  • 运行时间超过3分钟的任务,需要使用Worker。
  • 有关联的一系列同步任务,例如在需要创建和使用不同句柄的场景中,每次创建的句柄需要永久保存。这种情况需要使用Worker来管理线程生命周期。
  • 需要频繁取消任务的场景,例如图库大图浏览,为了提升用户体验,同时缓存当前图片左右侧各2张图片。当用户往一侧滑动跳到下一张图片时,需要取消另一侧的一个缓存任务。这种情况下,使用TaskPool来管理任务会更适合。Worker偏向线程的维度,支持长时间占据线程执行,需要主动管理线程的生命周期。
  • 需要长时间占用线程执行的任务,例如网络请求、数据库操作等。这种情况下,使用Worker可以保持线程的稳定性和性能。
  • 另外,在大量或者调度点较分散的任务场景下,如大型应用的多个模块包含多个耗时任务,不方便使用Worker去做负载管理,推荐采用TaskPool。
TaskPool运作机制

图片

  • TaskPool支持开发者在主线程封装任务抛给任务队列,系统会自动选择合适的工作线程,进行任务的分发及执行,再将结果返回给主线程
  • TaskPool提供简洁易用的接口,支持任务的执行和取消操作
  • 系统统一线程管理,结合动态调度及负载均衡算法,可以节约系统资源
Worker运作机制

  • Worker子线程与宿主线程拥有独立的实例,包含基础设施、对象、代码段
  • 每个Worker启动存在一定的内存开销,需要限制Worker的子线程数量
  • Worker子线程和宿主线程之间的通信是基于消息传递的
  • Worker通过序列化机制与宿主线程之间相互通信,完成命令及数据交互
TaskPool注意事项

@Concurrent装饰器:校验并发函数
  • 在HarmonyOS中,@Concurrent装饰器用于标识一个方法需要在工作线程中执行
  • 该装饰器可以应用于普通的方法或者回调方法
  • 使用@Concurrent装饰器的方法会在一个工作线程中执行,不会阻塞主线程的运行。
  • 对于一些耗时操作或者需要与其他服务进行交互的方法非常有用
  • 在方法执行完成后,可以使用HarmonyOS提供的线程间通信机制将结果传递回主线程
装饰器使用示例
import taskpool from '@ohos.taskpool';  
    @Concurrent  
    function add(num1: number, num2: number): number {  
      return num1 + num2  
    }  

    async function ConcurrentFunc(): Promise<void> {  
      try {  
        let task: taskpool.Task = new taskpool.Task(add, 1, 2)  
        console.log("taskpool res is:" + await taskpool.execute(task))  
      } catch (e) {  
        console.error("taskpool execute error is:" + e)  
      }  
    }

    @Entry  
    @Component  
    struct Index {  
      @State message: string = 'Submit';  

          build() {  

            RelativeContainer() {  

              Text(this.message)  
                .id('Submit')  
                .fontSize(50)  
                .fontWeight(FontWeight.Bold)  
                .onClick(() => {  
                    ConcurrentFunc()  
                })  
                .alignRules({  
                  center: { anchor: '__container__', align: VerticalAlign.Center },  
                  middle: { anchor: '__container__', align: HorizontalAlign.Center }  
                })  
            }  
            .height('100%')  
            .width('100%')  
          }
    }
同步任务
  • 在异步编程中,任务同步是指在多个异步任务之间进行协调和同步执行的过程。
  • 当存在多个异步任务需要按照一定的顺序或条件进行执行时,任务同步可以确保任务按照预期的顺序或条件进行执行。

常见的任务同步方式包括:

  • 回调函数:通过在一个异步任务完成后触发回调函数来执行下一个任务。
  • Promise/异步函数:使用Promise或异步函数的异步链式调用,通过then或await等关键字确保任务按顺序执行。
  • 线程间通信:通过消息队列或信号量等机制,在异步任务之间传递消息或信号,使得任务按特定的顺序或条件执行。
  • 锁或互斥体:使用锁或互斥体等同步机制,在异步任务之间实现互斥访问,确保任务按照顺序执行。
  • 任务同步的目的是确保异步任务能够按照一定的顺序或条件执行,以避免竞态条件、数据错误或逻辑错误。
使用taskpool处理同步任务
export default class Handle {  
      private static singleton : Handle  

      public static getInstance() : Handle {  
        if (!Handle.singleton) {  
          Handle.singleton = new Handle();  
        }  
        return Handle.singleton;  
      }  

      public syncGet() {  
        return  
      }  

      public static syncSet(num: number) {  
        return  
      }  
    }

    import taskpool from '@ohos.taskpool';
    import Handle from './Handle';

    // 定义并发函数,内部调用同步方法  
    @Concurrent  
    function func(num: number) {  
      // 调用静态类对象中实现的同步等待调用  
      Handle.syncSet(num)  
      // 或者调用单例对象中实现的同步等待调用  
      Handle.getInstance().syncGet()  
      return true  
    }  

    // 创建任务并执行  
    async function asyncGet() {  
      // 创建task并传入函数func  
      let task = new taskpool.Task(func, 1);  
      // 执行task任务,获取结果res  
      let res = await taskpool.execute(task);  
      // 对同步逻辑后的结果进行操作  
      console.info(String(res));  
    }

    asyncGet()

总结

  • 本次主要分享了关于鸿蒙开发中的异步并发和多线程并发的
  • 异步并发的介绍,和基本的用法,简单的举例;两种异步操作实现的对比
  • 多线程并发的简单概述,TaskPool和Worker两种多线程并发能力的介绍和对比,适用场景
  • 最后提及了TaskPool使用的简单例子

作者介绍

  • 吕游 资深Android开发工程师

微鲤技术团队

微鲤技术团队承担了中华万年历、Maybe、蘑菇语音、微鲤游戏高达3亿用户的产品研发工作,并构建了完备的大数据平台、基础研发框架、基础运维设施。践行数据驱动理念,相信技术改变世界。