写点什么

【HarmonyOS NEXT】ArkTS 线程模型解析与使用

作者:冉冉同学
  • 2024-12-30
    山东
  • 本文字数:5001 字

    阅读完需:约 16 分钟

【HarmonyOS NEXT】ArkTS 线程模型解析与使用

1. 前置学习文档

2. 前言

在了解 ArkTS 线程模型之前,先了解几组易混淆的概念。

2.1 名词解释

  • 进程(Process):进程是系统资源分配和调度的单元。一个运行着的程序就对应了一个进程。一个进程包括了运行中的程序和程序所使用到的内存和系统资源。

  • 目前 HarmonyOS 当前不支持手动创建进程;应用中(同一 Bundle 名称)的所有 UIAbility、ServiceExtensionAbility 和 DataShareExtensionAbility 等其它 Ability 均是运行在同一个独立进程(主进程)中;

  • 关于 HarmonyOS 中的进程模型,可点击这里阅读:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V13/process-model-stage-V13​线程(Thread):线程是进程下的执行者,一个进程至少开启一个线程(主线程),也可以开启多个线程。

  • **线程(Thread):**线程是进程下的执行者,一个进程至少开启一个线程(主线程),也可以开启多个线程。

  • 并行和并发的概念:

  • 并行(Parallelism):指程序的运行状态,在同一时间内有几件事情并行在处理。由于一个线程在同一时间只能处理一件事情,所以并行需要多个线程在同一时间执行多件事情。​

  • 并发(Concurrency):指程序的设计结构,在同一时间内多件事情能被交替地处理。重点是,在某个时间内只有一件事情在执行。比如单核 CPU 能实现多任务运行的过程就是并发。阻塞和非阻塞的概念:

  • 阻塞(Blocking):阻塞是指调用在等待的过程中线程被挂起(CPU 资源被分配到其他地方去)

  • 非阻塞(Non-blocking):非阻塞是指等待的过程 handlerCPU 资源还在该线程中,线程还能做其他的事情

  • 再来区分单线程和多线程的区别:

  • 单线程:从头执行到尾,逐行执行,如果其中一行代码报错,那么剩下代码将不再执行。同时容易代码阻塞。

  • 多线程:代码运行的环境不同,各线程独立,互不影响,避免阻塞。

  • 同步与异步的概念:

  • 同步(Synchronous):程序发出调用的时候,一直等待直到返回结果,没有结果之前不会返回。也就是,同步时调用者主动等待调用过程,且能立即得到结果的。​

  • 异步(Asynchronous):程序发出调用之后,无法立即得到结果,需要额外的操作才能得到预期的结果是为异步。

2.2 ArkTS 运行环境

  ArkCompiler eTS Runtime,类似于 Java 中的执行引擎 JVM【负责 Java 字节码的解析和执行】,Chrome Webkit 中 JavaScript 的执行引擎 V8 引擎【负责 JavaScript 代码的解析和执行】。


  它的大致流程就是先通过 ArkCompiler 将 ArkTS/TS/JS 程序预先静态编译为方舟字节码,在真机上使用 ets_runtime 运行方舟字节码【负责 ArkTS 字节码的解析和执行】。



3. 线程模型

本文涉及到的线程模型主要是基于官方的应用模型>Stage 模型。更多应用模型可点击这里:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V13/application-models-V13

3.1 概述

Stage 模型下的线程主要有如下三类:


  • 主线程

  • 执行 UI 绘制。

  • 管理主线程的 ArkTS 引擎实例,使多个 UIAbility 组件能够运行在其之上。

  • 管理其他线程的 ArkTS 引擎实例,例如使用 TaskPool(任务池)创建任务或取消任务、启动和终止 Worker 线程。

  • 分发交互事件。

  • 处理应用代码的回调,包括事件处理和生命周期管理。

  • 接收 TaskPool 以及 Worker 线程发送的消息。

  • TaskPool Worker线程

  • 用于执行耗时操作,支持设置调度优先级、负载均衡等功能,推荐使用。

  • Worker 线程

  • 用于执行耗时操作,支持线程间通信。

  • TaskPool 与 Worker 的运作机制、通信手段和使用方法可以参考TaskPool和Worker的对比。


3.2 Stage 模型>主线程

  从上述资料中,我们不难发现 ArkTS 的线程机制还是依托于 JavaScript 来实现的。而** JavaScript 是一门单线程语言**,所谓单线程,是指在 JS 引擎中负责解释和执行 JavaScript 代码的线程只有一个。不妨叫它主线程


  但是实际上还存在其他的线程。例如:处理 Http 网络请求的线程、处理 UI 事件的线程、定时器线程、读写文件的线程等等。这些线程可能存在于 JS 引擎之内,也可能存在于 JS 引擎之外,在此我们不做区分。不妨叫它们工作线程

3.3 同步与异步

  既然 js 是单线程,那么只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行,为了解决这个问题,JavaScript 语言将任务的执行模式分为两种:同步和异步。

3.3.1 同步

假设存在一个函数 A:


function A(args...);
复制代码


  如果在函数 A 返回的时候,调用者就能够得到预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。


例如:


let number=Math.abs(-666)console.log(number);//控制台输出 666console.log('你好');//控制台输出 你好
复制代码


  • 第一个函数返回时,就拿到了预期的返回值:666。

  • 第二个函数返回时,就看到了预期的效果:在控制台打印了一个字符串。


所以这两个函数都是同步的。

3.2.2 异步

假设存在一个函数 A:


function A(args...);
复制代码


  如果在函数 A 返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。


例如:


//执行一个异步的网络请求const session = rcp.createSession();session.post("http://example.com/post", "data to send").then((response) => {  console.info(`Succeeded in getting the response ${response}`);}).catch((err: BusinessError) => {  console.error(`err: err code is ${err.code}, err message is ${JSON.stringify(err)}`);});console.log('你好');//控制台输出 你好
复制代码


  在执行这段代码时,session.post 函数返回时,并不会立刻打印 response ,只有 session.post 底层 C++代码请求网络完成后才会返回到 then 或者 catch 中。也就是说异步函数 session.post 执行很快,不会阻塞 console.log('你好') 的执行,但后面还有工作线程执行异步任务、通知主线程、主线程回调等操作,这个过程就叫做异步过程。

3.4 消息队列与事件循环

  上面讲到,异步过程中,工作线程在异步操作完成后需要通知主线程。那么这个通知机制是怎样实现的呢?答案是利用消息队列和事件循环。


用一句话概括:


工作线程将消息放到消息队列,主线程通过事件循环过程去取消息。


  • 消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。

  • 事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。


   实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。


看到这个是不是觉得很熟悉,在 Android 中也有一套类似于这套事件循环的机制,就是 Handler: [https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/Handler.java]机制。


Kotlin 协程也有类似的实现类 kotlinx.coroutines.EventLoop: [https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/EventLoop.common.kt]

3.4.1 任务队列

  JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,可分为宏任务(Macrotask)微任务(Microtask)。)

3.4.1.1 任务(调用堆栈)

  JavaScript 代码执行时的函数调用会形成一个“调用堆栈”。当一个函数执行时,它会被添加到堆栈中,一旦完成,就会从堆栈中移除。

3.4.1.2 宏任务

事件的回调函数【比如 onClick、onScroll】、setTimeout、setInterval、I/O 操作、网络请求 等


  当异步事件(如用户点击、文件读取完成)发生时,与这些事件关联的回调函数会被添加到一个“任务队列”中。一旦调用堆栈为空,事件循环就会从队列中取出任务来执行。

3.4.1.3 微任务

promise.then 方法、Async/Await、Object.observe 等。


  另一种任务队列,用于处理诸如 Promise 回调这样的微任务(microtasks)。微任务队列在事件循环的每个阶段结束时都会被清空,这意味着微任务的优先级高于常规的异步任务(宏任务,macro-tasks)。

3.4.2 事件循环(Event Loop)

  首先,主线程会去执行所有的同步任务,此时可能会遇到需要异步处理的任务,例如 setTimeout、http 请求等,这需要交给其他线程处理,当其他线程处理完之后,再将回调函数放入任务队列。


  等到同步任务全部执行完,就会去看任务队列里面的异步任务。**如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。**一旦任务队列清空,程序就结束执行。


  JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。


 console.log('主线程开始!')​ setTimeout(function() {​     console.log('setTimeout完成!')​ }, 10);​ console.log('主线程结束!')​ //主线程开始!​ //主线程结束!​ //setTimeout完成!​ console.log('第一步');​setTimeout(() => {​  console.log('第二步');​}, 0);​console.log('第三步');​ //第一步​ //第三步​ //第二步
复制代码

3.4.2.1 事件循环流程图式





3.4.3 任务的执行顺序

在 ArkTS 中的事件循环由一个宏任务队列+多个微任务队列组成。


顺序是:同步任务 ---> 微任务 ---> 宏任务

3.4.3.1 案例演示 1-执行顺序详解

console.log(1);​setTimeout(() => {​  console.log(2);​}, 0);​const p = new Promise((resolve, reject) => {​  console.log(3);​  resolve("success"); // 标记为成功​  console.log(4);​});​p.then((value) => {​  console.log(value);​});​console.log(5);
复制代码


通过上述的理论解释,将上面代码分为同步任务、微任务、宏任务三块,



将我们分析好的代码,分开放在相应的队列里,可得到以下代码执行的流程图示:





所以这个案例,最终得到的执行结果就是: 1 3 4 5 “success” 2

3.4.3.2 案例演示 2-微任务的插队过程

setTimeout(()=>{​  console.log('第一个回调函数:宏任务1')​  Promise.resolve().then(()=>{​    console.log('第二个回调函数:微任务2')​  })​},0)​Promise.resolve().then(()=>{​  console.log('第三个回调函数:微任务1')​  setTimeout(()=>{​    console.log('第四个回调函数:宏任务2')​  },0)​})​// 第三个回调函数:微任务1​// 第一个回调函数:宏任务1​// 第二个回调函数:微任务2​// 第四个回调函数:宏任务2
复制代码


打印的结果不是 一、二、三、四。是因为,在 JavaScript 中,微任务的优先级比宏任务高,也就是说,如果微任务队列和宏任务队列中都有任务需要执行,微任务会先于宏任务执行。

4.总结

  • ArkTS 是单线程【ArkTS 引擎主线程】运行的。

  • ArkTS 中的异步任务**【setTimeout/setInterval/promise 等其它任务】** 中的任务执行也是在【ArkTS 引擎主线程】中运行的。如果运行耗时较长的任务,则会阻塞【ArkTS 引擎主线程】,导致 UI 在耗时任务结束之前无法操作,如果超过 6 秒及以上应用会崩溃,抛出 THREAD_BLOCK 或者 APP_INPUT_BLOCK 异常,例如下面的代码会抛出 APP_INPUT_BLOCK 异常


import { systemDateTime } from '@kit.BasicServicesKit'@Entry@Componentstruct Index {  @State message: string = 'ArkTS'  build() {    Column() {      Button('Run').onClick(() => {        let result = 0        let begin = systemDateTime.getTime()        for (let i = 1; i < 1000000000; ++i) {          result += (i + i) * i - i + 1.0 / i        }        let end = systemDateTime.getTime()        this.message = `${result}\n${(end - begin).toString()}`      })      Text(this.message)    }.width('100%')  }}
复制代码


发布于: 刚刚阅读数: 6
用户头像

冉冉同学

关注

还未添加个人签名 2018-05-12 加入

还未添加个人简介

评论

发布
暂无评论
【HarmonyOS NEXT】ArkTS 线程模型解析与使用_鸿蒙_冉冉同学_InfoQ写作社区