对ArkTS并发的理解

211 阅读15分钟

并发概述

并发是指在同一时间内,存在多个任务同时执行的情况。对于多核设备,这些任务可能同时在不同CPU并行执行。对于单核设备,多个并发任务不会在同一时刻并行执行,但是CPU会在某个任务休眠进行I/O操作等状态下切换任务,调度执行其他任务,提升CPU的资源利用率。

  • 异步并发
    • 代码执行到一定程度会被暂停
    • 同一时间只有一段代码在执行
    • 通过Promise和async/await提供异步并发能力
  • 多线程并发
    • 允许同一时间同时执行多段代码
    • UI主线程响应用户操作更新同时,后台线程也可以执行耗时操作
    • 通过TaskPool和Worker提供多线程并发能力

异步并发

Promise

Promise是一种用于处理异步操作的对象,可以将异步操作转换为类似于同步操作的方式,以方便代码编写和维护。

  • Promise 三种状态

    • pending(进行中)

      刚创建时处于此状态

    • fulfilled(已完成)

      操作成功后

    • rejected(已拒绝)

      操作失败后

  • 基本用法

    • 创建

      1. 使用new关键字创建一个Promise
      2. 使用setTimeout或其他异步 API 来模拟异步操作。
      3. Promise 接受一个 executor 函数,包含 resolvereject 回调
      4. 成功时调用 resolve,失败时调用 reject
      5. then 处理 resolve,catch 处理 reject。
      const promise: Promise<number> = new Promise((resolve: Function, reject: Function) => {
        setTimeout(() => {
          const randomNumber: number = Math.random();
          if (randomNumber > 0.5) {
            resolve(randomNumber);
          } else {
            reject(new Error('Random number is too small'));
          }
        }, 1000);
      })
      
    • 使用

      1. then可以接受两个参数then方法可接受两个参数,一个处理fulfilled状态的函数,另一个处理rejected状态的函数。此时上面的调用resolve后,返回第一个参数继续处理,上面的方法调用reject后返回第二个方法处理。
      2. 当只传一个参数时默认处理resolve后续事件,此时reject交由catch来处理。
      3. 当Promise被reject且未通过catch方法来处理时,会触发unhandledrejection事件,可使用errorManager.on('error')接口监听该事件,以全局捕获未处理的Promise reject。
      promise.then((result)=>{
        console.log("---->Promiss返回="+result);
      },(error: Error)=>{
        console.log("---->Promiss返回1="+error);
      }).catch((error: Error)=>{
        console.log("---->Promiss返回2="+error);
      });
      
  • async/await

    async/await是一种用于处理异步操作的Promise语法糖,它们可以让编写异步代码变得更加简单和易读。通过使用async关键字声明一个函数为异步函数,并使用await关键字等待Promise的解析(完成或拒绝),以同步的方式编写异步操作的代码,避免回调带来的逻辑嵌套。

  • async

    • 用于标识一个函数是异步函数
    • 用async标识的函数内部可以使用await关键字等待一个Promise对象的异步操作,并返回其结果。
  • await

    • 必须在async标识的函数内使用。
    • 表示以同步的形式,等待一个异步函数返回结果。
  • 示例

async myAsyncFunction(): Promise<string> {
  const result: Promise<string> = new Promise((resolve: Function) => {
    setTimeout(() => {
      resolve('Hello, world!');
    }, 3000);
  });
  return await result;
}

上面的示例中,使用await关键字来等待Promise的返回结果,并将结果存储在result中,返回给上一层。

多线程并发

概述

并发模型分为基于内存共享的并发模型和基于消息通信的并发模型。

  • 内存共享模型

    • 指多线程同时执行任务,
    • 这些线程依赖同一内存并且都有权限访问,
    • 线程访问内存前需要抢占并锁定内存的使用权,没有抢占到内存的线程需要等待其他线程释放使用权再执行。

    图示: image.png

    代码示例:

    1. 共享内存容器
    // 此段示例为伪代码仅作为逻辑示意,便于开发者理解使用内存共享模型和Actor模型的区别
    class Queue {
      // ...
      push(value: number) {}
    
      empty(): boolean {
        // ...
        return true
      }
    
      pop(value: number) :number {
        // ...
        return value;
      }
    }
    

    2. 内存锁

    class Mutex {
      // ...
      lock(): boolean {
        // ...
        return true;
      }
    
      unlock() {
    
      }
    }
    

    3. 操作管理类

    class BufferQueue {
      queue: Queue = new Queue()
      mutex: Mutex = new Mutex()
      add(value: number) {
        // 尝试获取锁
        if (this.mutex.lock()) {
          this.queue.push(value)
          this.mutex.unlock()
        }
      }
    
      take(value: number): number {
        let res: number = 0;
        // 尝试获取锁
        if (this.mutex.lock()) {
          if (this.queue.empty()) {
            res = 1;
          }
          let num: number = this.queue.pop(value)
          this.mutex.unlock()
          res = num;
        }
        return res;
      }
    }
    

    4. 全局共享的内存

    // 构造一段全局共享的内存
    let g_bufferQueue = new BufferQueue()
    
    class Producer {
      constructor() {
      }
      run() {
        let value = Math.random()
        // 跨线程访问bufferQueue对象
        g_bufferQueue.add(value)
      }
    }
    

    5. 消费者

    class ConsumerTest {
      constructor() {
      }
      run() {
        // 跨线程访问bufferQueue对象
        let num = 123;
        let res = g_bufferQueue.take(num)
        if (res != null) {
          // 添加消费逻辑
        }
      }
    }
    

    6. 生产者和使用示例

    function Main(): void {
      let consumer: ConsumerTest = new ConsumerTest()
      let producer1: Producer = new Producer()
      for (let i = 0;i < 0;i++) {
        // 模拟启动多线程执行生产任务
        // let thread = new Thread()
        // thread.run(producer.run())
        // consumer.run()
      }
    }
    
  • 消息通信模型

    • Actor
      • 基于消息通信的并发模型,不同角色之间并不共享内存没有锁困扰。
      • ArkTS提供了TaskPool和Worker两种并发能力
      • TaskPool和Worker都基于Actor并发模型实现
      • 生产者线程和UI线程都有自己的虚拟机实例,两个虚拟机实例之间拥有独占的内存,相互隔离。
      • 生产者生产出结果后通过序列化通信将结果发送给UI线程,
      • UI线程消费结果后再发送新的生产任务给生产者线程。

    图示: image.png

    代码示例:

    1. 生产者异步方法
     import { taskpool } from '@kit.ArkTS';
    // 跨线程并发任务
    @Concurrent
    async function produce(): Promise<number> {
      // 添加生产相关逻辑
      console.info("producing...");
      return Math.random();
    }
    

    2. 消费者类

    class Consumer {
      public consume(value: Object) {
        // 添加消费相关逻辑
        console.info("consuming value: " + value);
      }
    }
    

    3. 使用实例

    @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循环生成十个生产及消费任务
              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%')
      }
    }
    

TaskPool

任务池(TaskPool)作用是为应用程序提供一个多线程的运行环境,降低整体资源的消耗、提高系统的整体性能,且您无需关心线程实例的生命周期。具体接口信息及使用方法详情请见TaskPool

  • 运作机制

    • 宿主线程封装任务抛给任务队列,系统选择合适的工作线程,进行任务的分发及执行,再将结果返回给宿主线程

    • 支持任务的执行、取消,以及指定优先级的能力

    • 通过系统统一线程管理,结合动态调度及负载均衡算法,可以节约系统资源。

    • 系统默认会启动一个任务工作线程,当任务较多时会扩容,工作线程数量上限跟当前设备的物理核数相关,具体数量内部管理,保证最优的调度及执行效率,长时间没有任务分发时会缩容,减少工作线程数量。

      运作机制示意图: image.png

  • TaskPool注意事项

    • 实现任务的函数需要使用@Concurrent装饰器标注,且仅支持在.ets文件中使用。
    • 从API version 11开始,跨并发实例传递带方法的实例对象时,该类必须使用装饰器@Sendable装饰器标注,且仅支持在.ets文件中使用。
    • 任务函数在TaskPool工作线程的执行耗时不能超过3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),否则会被强制退出。
    • 实现任务的函数入参需满足序列化支持的类型,详情请参见线程间通信对象
    • ArrayBuffer参数在TaskPool中默认转移,需要设置转移列表的话可通过接口setTransferList()设置。
    • 由于不同线程中上下文对象是不同的,因此TaskPool工作线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。
    • 序列化传输的数据量大小限制为16MB。
    • Priority的IDLE优先级是用来标记需要在后台运行的耗时任务(例如数据同步、备份),它的优先级别是最低的。这种优先级标记的任务只会在所有线程都空闲的情况下触发执行,并且只会占用一个线程来执行。
    • Promise不支持跨线程传递,如果TaskPool返回pending或rejected状态的Promise,会返回失败;对于fulfilled状态的Promise,TaskPool会解析返回的结果,如果结果可以跨线程传递,则返回成功。
    • 不支持在TaskPool工作线程中使用AppStorage
  • @Concurrent装饰器

    在使用TaskPool时,执行的并发函数需要使用该装饰器修饰,否则无法通过相关校验。更多示例及说明请点我点我进入详细说明页。

Worker

Worker主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与宿主线程分离,在后台线程中运行一个脚本进行耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞宿主线程的运行。具体接口信息及使用方法详情请见Worker

  • 运作机制
    • 创建Worker的线程称为宿主线程(不一定是主线程,工作线程也支持创建Worker子线程)

    • Worker自身的线程称为Worker子线程(或Actor线程、工作线程)

    • 每个Worker子线程与宿主线程拥有独立的实例,包含基础设施、对象、代码段等,因此每个Worker启动存在一定的内存开销,需要限制Worker的子线程数量

    • Worker子线程和宿主线程之间的通信是基于消息传递的,Worker通过序列化机制与宿主线程之间相互通信,完成命令及数据交互。

      运作机制示意图

image.png

  • 使用方法

    • 创建Worker文件及配置
      • 手动创建
        1. 在{moduleName}/src/main/ets/workers/目录下创建work文件 import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';

          const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
          
          /**
           * Defines the event handler to be called when the worker thread receives a message sent by the host thread.
           * The event handler is executed in the worker thread.
           *
           * @param e message data
           */
          workerPort.onmessage = (e: MessageEvents) => {
            workerPort.postMessage()
          }
          
          /**
           * Defines the event handler to be called when the worker receives a message that cannot be deserialized.
           * The event handler is executed in the worker thread.
           *
           * @param e message data
           */
          workerPort.onmessageerror = (e: MessageEvents) => {
          }
          
          /**
           * Defines the event handler to be called when an exception occurs during worker execution.
           * The event handler is executed in the worker thread.
           *
           * @param e error message
           */
          workerPort.onerror = (e: ErrorEvent) => {
          }
          
        2. 配置build-profile.json5 "buildOption": { "sourceOption": { "workers": [ "./src/main/ets/workers/worker.ets" ] } }

      • 自动创建
        1. DevEco Studio支持一键生成Worker

          在对应的{moduleName}目录下任意位置,点击鼠标右键 > New > Worker,即可自动生成Worker的模板文件及配置信息,无需再手动在build-profile.json5中进行相关配置。

    • 加载Worker及通信

      • 导入模块

        import { worker } from '@kit.ArkTS';
        
      • 创建Worker

        const workerInstance: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/Worker.ets');
        
        
      • 发送消息到Worker

      workerInstance.postMessage({ type: 'start' });
      
      • 在Worker中接收消息
      workerPort.onmessage = (e: MessageEvents) => {
          const type = e.data.type as string;
          if (type === 'start') {
              performTask();
          }
      };
      
      • 关闭Worker

        当不再需要Worker时,应当将其关闭以释放资源。

        workerInstance.terminate();
        
  • Worker注意事项

    • 创建Worker时,有手动和自动两种创建方式,手动创建Worker线程目录及文件时,还需同步进行相关配置,详情请参考创建Worker的注意事项
    • 使用Worker能力时,构造函数中传入的Worker线程文件的路径在不同版本有不同的规则,详情请参见文件路径注意事项
    • Worker创建后需要手动管理生命周期,且最多同时运行的Worker子线程数量为64个,详情请参见生命周期注意事项
    • 由于不同线程中上下文对象是不同的,因此Worker线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。
    • 序列化传输的数据量大小限制为16MB。
    • 使用Worker模块时,需要在宿主线程中注册onerror接口,否则当Worker线程出现异常时会发生jscrash问题。
    • 不支持跨HAP使用Worker线程文件。
    • 引用HAR/HSP前,需要先配置对HAR/HSP的依赖,详见引用共享包
    • 不支持在Worker工作线程中使用AppStorage

TaskPool和Worker的对比

  1. 实现特点对比

表1 TaskPool和Worker的实现特点对比

实现TaskPoolWorker
内存模型线程间隔离,内存不共享。线程间隔离,内存不共享。
参数传递机制采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。
参数传递直接传递,无需封装,默认进行transfer。消息对象唯一参数,需要自己封装。
方法调用直接将方法传入调用。在Worker线程中进行消息解析并调用对应方法。
返回值异步调用后默认返回。主动发送消息,需在onmessage解析赋值。
生命周期TaskPool自行管理生命周期,无需关心任务负载高低。开发者自行管理Worker的数量及生命周期。
任务池个数上限自动管理,无需配置。同个进程下,最多支持同时开启64个Worker线程,实际数量由进程内存决定。
任务执行时长上限3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),长时任务无执行时长上限。无限制。
设置任务的优先级支持配置任务优先级。不支持。
执行任务的取消支持取消已经发起的任务。不支持。
线程复用支持。不支持。
任务延时执行支持。不支持。
设置任务依赖关系支持。不支持。
串行队列支持。不支持。
任务组支持。不支持。
  1. 适用场景对比

TaskPool和Worker均支持多线程并发能力。由于TaskPool的工作线程会绑定系统的调度优先级,并且支持负载均衡(自动扩缩容),而Worker需要开发者自行创建,存在创建耗时以及不支持设置调度优先级,故在性能方面使用TaskPool会优于Worker,因此大多数场景推荐使用TaskPool。

TaskPool偏向独立任务维度,该任务在线程中执行,无需关注线程的生命周期,超长任务(大于3分钟且非长时任务)会被系统自动回收;而Worker偏向线程的维度,支持长时间占据线程执行,需要主动管理线程生命周期。

常见的一些开发场景及适用具体说明如下:

  • 运行时间超过3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时)的任务。例如后台进行1小时的预测算法训练等CPU密集型任务,需要使用Worker。场景示例可参考常驻任务开发指导
  • 有关联的一系列同步任务。例如在一些需要创建、使用句柄的场景中,句柄创建每次都是不同的,该句柄需永久保存,保证使用该句柄进行操作,需要使用Worker。场景示例可参考使用Worker处理关联的同步任务
  • 需要设置优先级的任务。例如图库直方图绘制场景,后台计算的直方图数据会用于前台界面的显示,影响用户体验,需要高优先级处理,需要使用TaskPool。
  • 需要频繁取消的任务。例如图库大图浏览场景,为提升体验,会同时缓存当前图片左右侧各2张图片,往一侧滑动跳到下一张图片时,要取消另一侧的一个缓存任务,需要使用TaskPool。
  • 大量或者调度点较分散的任务。例如大型应用的多个模块包含多个耗时任务,不方便使用Worker去做负载管理,推荐采用TaskPool。场景示例可参考批量数据写数据库场景

并发线程间通信

线程间通信指的是并发多线程间存在的数据交换行为。

image.png

线程间通信对象

  • 普通对象

    • 普通对象跨线程时通过拷贝形式传递,两个线程的对象内容一致,但是指向各自线程的隔离内存区间,被分配在各自线程的虚拟机本地堆(LocalHeap)
    • 例如Ecmascript262规范定义的Object、Array、Map等对象是通过这种方式实现跨并发实例通信的。

    示例图:

    image.png

    • 定义普通对象 // Test.ets // 自定义class TestA export class TestA { constructor(name: string) { this.name = name; } name: string = 'ClassA'; }

    • 使用 // Index.ets import { taskpool } from '@kit.ArkTS'; import { BusinessError } from '@kit.BasicServicesKit'; import { TestA } from './Test';

      @Concurrent
      async function test1(arg: TestA) {
        console.info("TestA name is: " + arg.name);
      }
      
      @Entry
      @Component
      struct Index {
        @State message: string = 'Hello World';
      
        build() {
          RelativeContainer() {
            Text(this.message)
              .id('HelloWorld')
              .fontSize(50)
              .fontWeight(FontWeight.Bold)
              .alignRules({
                center: { anchor: '__container__', align: VerticalAlign.Center },
                middle: { anchor: '__container__', align: HorizontalAlign.Center }
              })
              .onClick(() => {
                // 1. 创建Test实例objA
                let objA = new TestA("TestA");
                // 2. 创建任务task,将objA传递给该任务,objA非sendable对象,通过序列化传递给子线程
                let task = new taskpool.Task(test1, objA);
                // 3. 执行任务
                taskpool.execute(task).then(() => {
                  console.info("taskpool: execute task success!");
                }).catch((e:BusinessError) => {
                  console.error(`taskpool: execute task: Code: ${e.code}, message: ${e.message}`);
                })
              })
          }
          .height('100%')
          .width('100%')
        }
      }
      
  • ArrayBuffer对象

    请看官方文档

  • SharedArrayBuffer对象

    请看官方文档

  • Transferable对象

    请看官方文档

  • Sendable对象

    请看官方文档