多線程任務開發范例-Worker
概念介紹
在和應用界面進行交互操作時,如按鈕點擊、屏幕滑動,想同時執行一些耗時的操作,如網絡請求、數據下載。在應用開發中,通常使用UI線程和后臺線程來分別處理這些操作,UI線程主要負責處理UI事件和用戶交互操作,后臺線程負責耗時操作。通過創建后臺線程可以避免UI線程被阻塞,提高應用程序的響應速度和用戶體驗。
OpenHarmony的ArkUI應用開發框架提供了Worker和Taskpool等支持后臺多線程任務的方式,本文會通過開發范例介紹Worker的使用。在ArkUI應用開發中,有2類線程:宿主線程和Worker線程。創建Worker的線程被稱為宿主線程,Worker腳本程序工作的線程被稱為Worker線程。Worker線程是與主線程并行的獨立線程,通常在Worker線程中處理耗時的操作。需要注意的是,在Worker后臺線程中執行的代碼不能直接修改UI元素,UI元素的更新必須發生在UI線程中。
API接口
ArkUI的Worker線程模塊提供了構造函數接口用于創建Worker線程,并為UI線程和Worker線程提供了線程間通訊接口。關于Worker API能力詳細信息,請參考@ohos.worker。本節只進行關鍵接口解讀。
宿主線程中的構造函數
使用Worker的接口方法前,需要先構造ThreadWorker實例,ThreadWorker類繼承WorkerEventTarget。
注意:Worker還提供構造函數worker.Worker(scriptURL: string, options?: WorkerOptions),由于已經標記廢棄,請避免使用該廢棄的接口。
ThreadWorker構造函數如下:
constructor(scriptURL: string, options?: WorkerOptions)
其中,參數解釋:
參數名 | 類型 | 必填 | 說明 |
scriptURL | string | 是 | Worker執行腳本的路徑 |
options | 否 | Worker構造的選項。 |
我們來看一個構造的示例。不用擔心其中的腳步文件如何編寫,使用DevEco Studio創建Worker文件的時候,會生成模板。
import worker from '@ohos.worker';
// worker線程創建
// Stage模型-目錄同級(entry模塊下,workers目錄與pages目錄同級)
const workerStageModel01 = new worker.ThreadWorker('entry/ets/workers/worker.ts', {name:"first worker in Stage model"});
// Stage模型-目錄不同級(entry模塊下,workers目錄是pages目錄的子目錄)
const workerStageModel02 = new worker.ThreadWorker('entry/ets/pages/workers/worker.ts');
宿主線程中發送消息
宿主線程通過轉移對象所有權或者拷貝數據的方式向Worker線程發送消息,提供了兩個postMessage<sup>9+</sup>接口,其中一個如下所示:
postMessage(message: Object, options?: PostMessageOptions): void
其中,參數如下:
參數名 | 類型 | 必填 | 說明 |
message | Object | 是 | 發送至Worker的數據,該數據對象必須是可序列化。 |
options | PostMessageOptions | 否 | 當填入該參數時,與傳入ArrayBuffer[]的作用一致,該數組中對象的所有權會被轉移到Worker線程, 在宿主線程中將會變為不可用,僅在Worker線程中可用。 若不填入該參數,默認設置為 undefined,通過拷貝數據的方式傳輸信息到Worker線程。 |
示例代碼如下:
const workerInstance = new worker.ThreadWorker("entry/ets/workers/worker.ts");
workerInstance.postMessage("hello world");
var buffer = new ArrayBuffer(8);
workerInstance.postMessage(buffer, [buffer]);
宿主線程中監聽消息
在宿主線程中,通過監聽事件來處理接收到的Worker線程中的消息。worker模塊提供了若干監聽接口,我們以onmessage為例進行講解,其他監聽方式類似,可以參考API參考文檔,不再贅述。
Worker對象的onmessage屬性表示宿主線程接收到來自其創建的Worker通過parentPort.postMessage接口發送的消息時被調用的事件處理程序,處理程序在宿主線程中執行。
onmessage?: (event: MessageEvents) => void
其中,參數如下:
參數名 | 類型 | 必填 | 說明 |
event | MessageEvents | 是 | 收到的Worker消息數據。 |
示例代碼如下:
const workerInstance = new worker.ThreadWorker("entry/ets/workers/worker.ts");
workerInstance.onmessage = function(e) {
// e : MessageEvents, 用法如下:
// let data = e.data;
console.log("onmessage");
}
Worker線程中構造實例
ThreadWorkerGlobalScope是Worker線程用于與宿主線程通信的類,通過postMessage接口發送消息給宿主線程、通過close接口銷毀Worker線程。ThreadWorkerGlobalScope類繼承GlobalScope9+。
注意:Worker還提供worker.parentPort接口,該接口屬于廢棄接口,應避免使用。
在Worker腳本文件中,如entry\src\main\ets\workers\Worker.ts,構建實例如下:
import worker, { ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from '@ohos.worker';
var workerPort: ThreadWorkerGlobalScope = worker.workerPort;
Worker線程中監聽消息
ThreadWorkerGlobalScope的onmessage屬性表示Worker線程收到來自其宿主線程通過postMessage接口發送的消息時被調用的事件處理程序,處理程序在Worker線程中執行。
onmessage?: (this: ThreadWorkerGlobalScope, ev: MessageEvents) => void
其中,參數如下所示:
參數名 | 類型 | 必填 | 說明 |
this | ThreadWorkerGlobalScope | 是 | 指向調用者對象。 |
ev | MessageEvents | 是 | 收到宿主線程發送的數據。 |
示例代碼如下:
// main thread
import worker from '@ohos.worker';
const workerInstance = new worker.ThreadWorker("entry/ets/workers/worker.ts");
workerInstance.postMessage("hello world");
// worker.ts
import worker from '@ohos.worker';
const workerPort = worker.workerPort;
workerPort.onmessage = function(e) {
console.log("receive main thread message");
}
Worker線程中發送消息
Worker線程通過轉移對象所有權或者拷貝數據的方式向宿主線程發送消息。提供了兩個postMessage9+接口,其中一個如下所示:
postMessage(messageObject: Object, options?: PostMessageOptions): void
其中,參數如下所示:
參數名 | 類型 | 必填 | 說明 |
message | Object | 是 | 發送至宿主線程的數據,該數據對象必須是可序列化,序列化支持類型見其他說明。 |
options | PostMessageOptions | 否 | 當填入該參數時,與傳入ArrayBuffer[]的作用一致,該數組中對象的所有權會被轉移到宿主線程,在Worker線程中將會變為不可用,僅在宿主線程中可用。<br/>若不填入該參數,默認設置為 undefined,通過拷貝數據的方式傳輸信息到宿主線程。 |
線程的關閉和銷毀
銷毀worker的方式有兩種;
- 被動銷毀
worker線程的生命周期跟隨應用。若應用退出則釋放worker資源。worker線程在執行過程中出現異常終止掉worker。
- 主動銷毀
主動銷毀worker的方式有兩種,第一種在宿主線程調用worker.terminate()
;第二種在worker線程調用workerPort.close()
。 worker銷毀前會觸發onexit回調,注意,onexit回調只會在宿主線程中執行。
宿主線程中銷毀worker線程的示例代碼:
const worker = new worker.ThreadWorker("entry/ets/workers/worker.ts");
worker.terminate();
Worker線程中銷毀worker線程的示例代碼:
// worker.ts
import worker from '@ohos.worker';
const workerPort = worker.workerPort;
workerPort.onmessage = function(e) {
workerPort.close()
}
實現場景
我們模擬一個簡單的UI線程和Worker線程交互的場景。UI線程發送一個簡單的消息給Worker線程,觸發Worker線程中的一個耗時模擬操作,然后把結果返回UI線程進行界面展示。有點像,一個人站在山谷前,大喊一聲,過一段時間會從山谷中返回聲音。這個人就是UI線程,返回回音的山谷就是后臺線程。
設計思路
對于UI線程,只需要簡單地包含一個text和一個button。text用于展示后臺線程返回的信息,button按鈕被點擊后向后臺線程發送消息。UI線程還需要處理后臺返回的消息。
對于后臺線程,需要處理接收到UI消息,模擬一個耗時操作,然后返回。實現效果如下:
發送消息前 | 等待返回 | 消息返回 |
|
|
|
開發步驟
創建Worker
DevEco Studio提供了非常方便的創建Worker的方法。
在DevEco Studio工程中,選擇entry,右鍵菜單選擇New-Worker,輸入Worker名稱即可,比如就使用默認的Worker。
Studio會自動為生成文件entry\src\main\ets\workers\Worker.ts,并在模塊級配置文件entry\build-profile.json5中添加workers配置,如圖所示,可以看出使用的相對路徑:‘./src/main/ets/workers/Worker.ts’。
文件entry\build-profile.json5片段:
"buildOption": {
"sourceOption": {
"workers": [
'./src/main/ets/workers/Worker.ts',
]
}
},
宿主進程代碼實現
我們先看下宿主進程中,代碼如何實現。
我們知道,Worker線程不可以直接操作UI。在宿主線程中,監聽到的worker線程返回消息無法直接賦值給@State變量進行UI界面渲染的。需要通過其他方式進行傳值,本示例中我們使用AppStorage和@Watch裝飾器。
如代碼所示,創建一個workerResult變量,當該變量發生變化后,會通過執行監聽函數workerResultChanged(),把存儲的值賦值給@State變量。
在宿主線程中創建的worker實例為threadWorker,它負責通過腳本文件創建worker線程,并負責執行和worker線程的通訊交互。
在宿主線程中,界面中包含一個文本,展示文字,如果從worker進程中接收到的消息等,還有一個按鈕,點擊時會觸發發worker線程發送消息。
在Button的onClick()函數中,主要實現了2個功能,一個是定義宿主線程接收到worker消息的回調函數。從代碼中可以看出,當接收到消息后,會保存到AppStorage里。
另外一個功能點是,通過調用postMessage接口,向worker線程發送消息。
在宿主線程中,還支持很多監聽函數,限于篇幅,不再展示,可以參考API自行實現。
import worker from '@ohos.worker';
let workerResult = AppStorage.Link('workerResult')
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
@StorageLink('workerResult') @Watch('workerResultChanged') workerResult: String = ''
threadWorker: worker.ThreadWorker = new worker.ThreadWorker("entry/ets/workers/Worker.ts")
workerResultChanged() {
this.message = AppStorage.Get('workerResult')
}
build() {
Row() {
Column() {
Text(this.message)
.fontSize(20)
.fontWeight(FontWeight.Bold)
Button('Click').onClick(
() => {
this.threadWorker.onmessage = function (message) {
AppStorage.Set<String>('workerResult', message.data)
}
this.threadWorker.postMessage("message from main thread.")
}
)
}
.width('100%')
}
.height('100%')
}
}
Worker進程代碼實現
我們再看下Worker進程中,代碼如何實現。Work線程腳本文件entry\src\main\ets\workers\Worker.ts。
語句var workerPort: ThreadWorkerGlobalScope = worker.workerPort;用于構建Worker線程中的實例對象,該實例可以與宿主線程進行消息交互。
在workerPort.onmessage監聽函數中,控制臺打印輸出從宿主線程中接收到的消息,然后通過workerPort.postMessage接口向宿主線程第一次發送消息,告訴宿主線程
請等待worker線程的操作。
然后,使用setTimeout函數模擬一個耗時操作,5000ms后再次向宿主線程發送消息,攜帶一個隨機數字,用于區分多次返回消息的差異。
在worker線程中的其他監聽函數,如workerPort.onmessageerror、workerPort.onerror,或者銷毀worker線程的操作可以參考API自行實現。
文件entry\src\main\ets\workers\Worker.ts片段:
import worker from '@ohos.worker';
import { ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from '@ohos.worker';
var workerPort: ThreadWorkerGlobalScope = worker.workerPort;
workerPort.onmessage = function (e: MessageEvents) {
console.info("onmessage: " + e.data)
workerPort.postMessage("Waiting for the worker ...")
setTimeout(() => {
console.info('send to main thread')
workerPort.postMessage("Echo from worker Random: "
+ Math.round(100 * Math.random()))
},
5000)
}
運行測試效果
代碼編寫完畢,可以測試運行查看效果。推薦在模塊級配置文件entry\build-profile.json5中,修改運行時為"HarmonyOS",這樣就可以在DevEco Studio中使用Simulator模擬器進行運行測試,手頭沒有設備也可以輕松體驗OpenHarmony應用開發。
注意事項
Worker線程不可以直接操作UI,@State等變量無法直接進行賦值渲染,需要通過其他方式進行傳值。在本開發范例中, 就借助了AppStorage。
Worker線程不使用時,請及時銷毀,避免耗用資源。Worker有資源限制,如果創建數量太多,可以報如下錯誤:
Error message: Worker initialization failure, the number of workers exceeds the maximum.
SourceCode:
this.threadWorker = new worker.ThreadWorker("entry/ets/workers/Worker.ts");