TaskPool provides a series of concurrent APIs based on the pooling concept and task mechanism. It aims to fully utilize the advantages of multi-core CPUs, reduce the load of the main thread, and improve program performance. When using TaskPool for development, you need to follow certain guidelines and use it in different scenarios based on the service and concurrency features. Non-compliance with these guidelines may cause performance deterioration, stability issues, or other unexpected problems.
This document analyzes issues such as application errors, service exceptions, and excessive resource consumption caused by incorrect use of TaskPool, and summarizes some usage guidelines to help you better use TaskPool for application development.
Scenario
When files and HARs are imported into a project, some files use UI decorators or state variables such as @Observed and AppStorage. These UI decorators or state variables may be parsed and executed even if they are not explicitly called. However, child threads do not support UI properties. When these UI decorators or state variables are parsed, an exception is thrown and returned, interrupting the parsing of the module. When certain variables in these UI-containing files are accessed, an error xxx is not initialized may be thrown, leading to feature failure or crashes. See the following figure:

In some complex projects, this issue may also occur due to changes in SDKs or other dependent modules, even if the current module remains unchanged. Troubleshooting such an issue can be challenging. Therefore, it is recommended to implement appropriate constraints and verification during the development and iteration phases.
Negative example
Implement the Foo and Bar classes.
// a.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
@Observed
class Foo {
constructor() {
hilog.info(0xFF00, "sampleTag", "this is class Foo");
}
}
export class Bar {
constructor() {
hilog.info(0xFF00, "sampleTag", "this is class Bar");
}
} Import the required classes and methods in the Sample1.ets file.
// Sample1.ets
import { Bar } from './wrong/a';
import { BusinessError } from '@ohos.base';
import { taskpool } from '@kit.ArkTS';
import { Foo } from './correct/b';
import { hilog } from '@kit.PerformanceAnalysisKit'; Implement a concurrent function decorated with @Concurrent.
// Sample1.ets
@Concurrent
function wrongConcurrentFunc() {
try {
let bar: Bar = new Bar;
} catch (e) {
let error: BusinessError = e;
hilog.error(0xFF00, 'sampleTag', 'error occur: ' + error.message);
}
} Use taskpool.execute() in the onClick() method of the component to call wrongConcurrentFunc().
// Sample1.ets
taskpool.execute(wrongConcurrentFunc)
.then(() => {
hilog.info(0x000, 'testTag', 'execute success');
})
.catch((err: BusinessError) => {
hilog.error(0x000, 'testTag', `execute failed, code=${err.code}, message=${err.message}`);
}) In this example, two classes (Foo and Bar) are declared in a.ets, and class Foo is decorated with @Observed. When you attempt to create a new Bar object in a child thread, an exception is thrown when the @Observed decorator is parsed and executed, and the logic of class Bar is not executed. As a result, Bar is an undefined variable, and the exception Bar is not initialized is thrown when you access it.
Positive example
Implement the class Bar decorated with @Observed.
// a.ets
@Observed
export class Bar {
id: number = 0;
name: string = "Bar";
} Implement a common class Foo.
// b.ets
export class Foo {
id: number = 0;
} Create a Foo object in the correctConcurrentFunc() method.
// Sample1.ets
@Concurrent
function correctConcurrentFunc() {
let foo: Foo = new Foo;
} Use taskpool.execute() in the onClick() method of the component to call correctConcurrentFunc().
// Sample1.ets
taskpool.execute(correctConcurrentFunc)
.then(() => {
hilog.info(0x000, 'testTag', 'execute success');
})
.catch((err: BusinessError) => {
hilog.error(0x000, 'testTag', `execute failed, code=${err.code}, message=${err.message}`);
}) Move the class (Bar in this example) that involves UI to a separate file, and then import the class (Foo in this example) that does not involve UI in the child thread. This ensures that the application runs correctly. This structure helps improve program correctness, execution efficiency, and maintainability.
Scenario
Generally, listeners are long-lived. When you register a listener in a child thread and event callback is executed, the listener still takes effect even after the task execution is complete, and the triggering time is uncertain. Since TaskPool enables the load balancing mechanism, idle threads are reclaimed after the non-continuous tasks are executed.
If the child thread where the listener is registered has been released and another thread sends an event to this child thread, an exception or undefined behavior, such as a crash, may occur.
Therefore, if you need to register a listener, you are advised to use continuous tasks to proactively manage the lifecycle of the thread where the task is running.

Negative example
Call the listener method in the httpRequest object in the concurrentFunc() method to listen for the network request result.
// Sample2.ets
import { http } from '@kit.NetworkKit';
import { taskpool } from '@kit.ArkTS';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Concurrent
function concurrentFunc() {
let httpRequest = http.createHttp();
httpRequest.on('headersReceive', (header: Object) => {
hilog.info(0xFF00, 'sampleTag', 'header: ' + JSON.stringify(header));
});
httpRequest.on('dataEnd', () => {
hilog.info(0xFF00, 'sampleTag', 'No more data in response, data receive end');
});
} Use taskpool.execute() in the onClick() method of the component to call concurrentFunc().
// Sample2.ets
taskpool.execute(concurrentFunc)
.then(() => {
hilog.info(0x000, 'testTag', 'execute success');
})
.catch((err: BusinessError) => {
hilog.error(0x000, 'testTag', `execute failed, code=${err.code}, message=${err.message}`);
}) In the preceding negative example, the HTTP API is used in concurrentFunc(). The on() API is called to listen for the headersReceive event, and the callback is triggered when the event occurs. From the perspective of concurrentFunc(), after the on() API is called, the logic of the task is complete and the result is returned. If the callback is not triggered, the thread executing the task remains idle. After a period of time, the thread may be released. If the headersReceive event occurs again at this time, unexpected behavior may occur.
Positive example
Call the listener method in the httpRequest object in the concurrentFunc() method to listen for the network request result.
// Sample2.ets
import { http } from '@kit.NetworkKit';
import { taskpool } from '@kit.ArkTS';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Concurrent
function concurrentFunc() {
let httpRequest = http.createHttp();
httpRequest.on('headersReceive', (header: Object) => {
hilog.info(0xFF00, 'sampleTag', 'header: ' + JSON.stringify(header));
});
httpRequest.on('dataEnd', () => {
hilog.info(0xFF00, 'sampleTag', 'No more data in response, data receive end');
});
} In the onClick() method of the component, use taskpool.execute() to execute the concurrentFunc() method and interrupt the task after the callback result is obtained.
// Sample2.ets
let task: taskpool.LongTask = new taskpool.LongTask(concurrentFunc);
taskpool.execute(task)
.then(() => {
hilog.info(0xFF00, 'sampleTag', 'receive http msg success.');
taskpool.terminateTask(task);
})
.catch((err: BusinessError) => {
hilog.error(0x000, 'testTag', `execute failed, code=${err.code}, message=${err.message}`);
}) Select a task type based on service requirements. Continuous tasks are suitable for HTTP listening APIs. In addition, you need to proactively manage the lifecycle of the thread where the task is running. After the specific logic is executed, call the terminateTask() API (usually called when the result is obtained, that is, after await or in the then logic) to release resources. This prevents the thread that executes the continuous task from remaining alive for a long time.
Scenario
TaskPool provides the sendData() API for communication between the TaskPool child thread and the host thread. As an API provided by TaskPool, sendData() can securely transfer data from a child thread to the host thread.
However, the sendData() API depends on Task, and its lifecycle is the same as that of Task. Considering the characteristics of microtasks and asynchronous events, a callback may be processed only after the TaskPool task returns its result. In this case, Task may have already been destroyed. It is unreasonable and insecure to call the sendData() API that depends on Task. In this case, TaskPool throws an exception. If the exception is thrown after the task result is returned, it will be left in the thread and not processed. Therefore, you should avoid using sendData() in the callback.
If necessary, you are advised to use the emitter, which can easily implement bidirectional communication between the host thread and child threads. In addition, the on() API of emitter is listener-based, and the listener can be triggered at any time if the registration is not canceled. For this reason, you need to register it in LongTask.

Negative example
Implement a concurrent function decorated with @Concurrent and asynchronously call the sendData() method in TaskPool using a promise.
// Sample3.ets
@Concurrent
function wrongConcurrentFunc() {
let promise = Promise.resolve();
promise.then(() => {
try {
taskpool.Task.sendData();
} catch (error) {
let err = error as BusinessError;
hilog.warn(0x000, 'testTag', `sendData failed, code=${err.code}, message=${err.message}`);
}
})
} Create a Task object and use the onReceiveData() method to receive data sent from TaskPool through sendData().
// Sample3.ets
let task: taskpool.Task = new taskpool.Task(wrongConcurrentFunc);
task.onReceiveData(() => {
hilog.info(0xFF00, 'sampleTag', "onReceiveData has been called");
})
taskpool.execute(task)
.then(() => {
hilog.info(0xFF00, 'sampleTag', 'receive http msg success.');
taskpool.terminateTask(task);
})
.catch((err: BusinessError) => {
hilog.error(0x000, 'testTag', `execute failed, code=${err.code}, message=${err.message}`);
}) In the preceding negative example, an asynchronous API is used, and sendData() is used in then. In the execution flow, wrongConcurrentFunc() first calls Promise.resolve() to generate and return a promise. In this case, there is no other executable logic, so the function directly returns, that is, it exits the scope of wrongConcurrentFunc(). When the microtask queue is executed, the callback logic in then is executed, but it is not executed within the scope of wrongConcurrentFunc(). As a result, the error message "sendData is not called in the concurrent function" is thrown.
Positive example
Implement a concurrent function decorated with @Concurrent and use the emitter() method to send data.
// Sample3.ets
@Concurrent
function correctConcurrentFunc() {
let promise = Promise.resolve();
promise.then(() => {
emitter.emit("1", { data: { name: "anonymous" } });
})
} Create a LongTask object and interrupt the task after emitter() receives data.
// Sample3.ets
let task: taskpool.LongTask = new taskpool.LongTask(correctConcurrentFunc);
emitter.on("1", (data: emitter.EventData) => {
hilog.info(0xff00, 'sampleTag', "name is : " + data.data?.name);
emitter.off("1")
taskpool.terminateTask(task)
})
taskpool.execute(task)
.then(() => {
hilog.info(0xFF00, 'sampleTag', 'receive http msg success.');
taskpool.terminateTask(task);
})
.catch((err: BusinessError) => {
hilog.error(0x000, 'testTag', `execute failed, code=${err.code}, message=${err.message}`);
}) In the positive example, the usage of promise.then is still used. The difference from the negative example is that emitter() is used instead of the sendData() API in the then logic. In addition, the correctConcurrentFunc() task is declared as a LongTask and use terminateTask() to manually manage the lifecycle, ensuring both functionality and program correctness.
Scenario
Using TaskPool has certain execution costs. Time-consuming tasks (in the synchronous or asynchronous callback phase) can be executed in TaskPool, while short-time tasks can be directly executed in the main thread.
In addition, you should split services at an appropriate granularity. A task group can be used for a collection of associated tasks. You need to wait until all subtasks are processed before proceeding to the next step, ensuring the integrity and maintainability of the service code. For some non-urgent query tasks, you can collect them and execute them in the task pool during idle periods.
In appropriate scenarios, you can also use APIs such as SequenceRunner() to prevent a large number of tasks from entering the task pool consecutively within a short period of time, which would cause the number of threads to instantly increase to the maximum. If a high volume of task result callbacks and microtasks flood back to the main thread in quick succession, continuous processing of these events can block the UI and impair user experience.

// Sample5.ets
import { taskpool } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
@Concurrent
function imageProcessing(dataSlice: ArrayBuffer): ArrayBuffer {
// Step 1: Specific image processing operations and other time-consuming operations.
return dataSlice;
}
function histogramStatistic(pixelBuffer: ArrayBuffer): void {
// Step 2: Divide into three segments for concurrent scheduling.
let number: number = pixelBuffer.byteLength / 3;
let buffer1: ArrayBuffer = pixelBuffer.slice(0, number);
let buffer2: ArrayBuffer = pixelBuffer.slice(number, number * 2);
let buffer3: ArrayBuffer = pixelBuffer.slice(number * 2);
let group: taskpool.TaskGroup = new taskpool.TaskGroup();
try {
group.addTask(imageProcessing, buffer1);
group.addTask(imageProcessing, buffer2);
group.addTask(imageProcessing, buffer3);
} catch (error) {
let err = error as BusinessError;
hilog.warn(0x000, 'testTag', `addTask failed, code=${err.code}, message=${err.message}`);
}
taskpool.execute(group, taskpool.Priority.HIGH)
.then((ret: Object) => {
// Step 3: Summarize the result array.
})
.catch((err: BusinessError) => {
hilog.error(0x000, 'testTag', `execute failed, code=${err.code}, message=${err.message}`);
})
} Call histogramStatistic() in the onClick() method of the component.
// Sample5.ets let buffer: ArrayBuffer = new ArrayBuffer(24); histogramStatistic(buffer);
The preceding example is a piece of code for processing images. In the example, the image buffer is divided into three segments. The TaskGroup API of TaskPool is used to distribute a group of tasks to multiple child threads for computation, receive multiple results at the same time, and return the results to the UI thread for display.
Scenario
In the TaskPool concurrency scenario, you should ensure that the APIs are matched. For example, the open() API must be used together with close() API, and clearInterval() must be called after setInterval() is used. If the APIs do not match, some handles may not be properly closed during the exit phase. As a result, the thread cannot be released. When there are a large number of threads, the resident memory is greatly affected.
You are advised to use try...catch... to handle exceptions that may occur in the service logic. For example, if the parameters passed to taskpool.execute() may be abnormal, use the outer try...catch... to capture the exception in a timely manner. If the task in the child thread may be abnormal, use .catch to capture the exception.
Example 1
Create a concurrent function decorated by @Concurrent and use setInterval() to simulate a scheduled task.
// Sample4.ets
@Concurrent
function correctConcurrentFunc() {
let count: number = 0;
let id = setInterval(() => {
count++;
if (count == 10) {
hilog.info(0xFF00, 'sampleTag', "the value has reached the threshold");
clearInterval(id);
}
}, 1000);
} In the onClick() method of the component, execute the correctConcurrentFunc() method through TaskPool.
// Sample4.ets
let task: taskpool.Task = new taskpool.Task(correctConcurrentFunc);
taskpool.execute(task)
.then(() => {
hilog.info(0xFF00, 'sampleTag', 'receive http msg success.');
})
.catch((err: BusinessError) => {
hilog.error(0x000, 'testTag', `execute failed, code=${err.code}, message=${err.message}`);
}) In the child thread, the timer setInterval() is used to simulate a scheduled task. When the time condition is met, clearInterval() is called to cancel the timer, ensuring that the thread can be properly released when it is idle.
Example 2
Create a class decorated with @Observed and pass an object of this class to the concurrent function decorated with @Concurrent.
// Sample4.ets
@Observed
class Foo {
id: number = 0;
name: string = "foo"
}
@Concurrent
function correctConcurrentFunc1(foo: Foo) {
console.info("the id is: " + foo.id);
} In the onClick() method of the component, execute correctConcurrentFunc1() through TaskPool and use try...catch... to capture exceptions.
// Sample4.ets
try {
let foo = new Foo();
taskpool.execute(correctConcurrentFunc1, foo)
.then(() => {
hilog.info(0xFF00, 'sampleTag', 'receive http msg success.');
})
.catch((err: BusinessError) => {
hilog.error(0x000, 'testTag', `execute failed, code=${err.code}, message=${err.message}`);
})
} catch (e) {
let error: ErrorEvent = e;
hilog.error(0xFF00, 'sampleTag', "error info: " + error.message);
} When creating and executing Task, simulate a serialization error scenario where the class Foo decorated with @Observed is incorrectly passed. Generally, the exceptions thrown by TaskPool are captured using .catch. However, for the serialization logic in taskpool.execute(), the promise has not been created yet. As a result, the exception cannot be captured by .catch. In this case, the outer try...catch... is used to capture the exception.
Example 3
Use @Concurrent to implement a method and throw an exception.
// Sample4.ets
@Concurrent
function correctConcurrentFunc2() {
let error: Error = new Error("TaskPoolThread error");
throw error;
} In the onClick() method of the component, execute the correctConcurrentFunc2() method through TaskPool.
// Sample4.ets
taskpool.execute(correctConcurrentFunc2).catch((error: BusinessError) => {
hilog.error(0xFF00, 'sampleTag', "error info: " + error.message);
}) In example 3, .catch is used to handle the exception thrown by the child thread. The exception can be correctly captured, and the printed information meets the expectation.
Scenario
Currently, serialization supports the following data types: regular object, ArrayBuffer object, SharedArrayBuffer object, Transferable object (NativeBinding object), and Sendable object. Other types, such as proxy and promise, are not supported. If a failure is caused by serialization, the log contains "taskpool: failed to serialize arguments." or "taskpool: failed to serialize result." You can filter ArkCompiler error logs to view the specific error type and troubleshoot code based on the error type. (The return value transfer also does not support these types.)
Negative example 1
Create a concurrent function decorated with @Concurrent and dynamically import a module.
// Sample6.ets
@Concurrent
async function returnModule() {
let module = await import('./a');
return module;
} In the onClick() method of the component, execute the returnModule() method through TaskPool and use try...catch... to capture the exception.
// Sample6.ets
taskpool.execute(returnModule).catch((e: BusinessError) => {
hilog.error(0xFF00, 'sampleTag', "error info: " + e.message);
}) In negative example 1, a module is dynamically imported and loaded, and then returned to the main thread for use. An error is reported because the module does not support serialization.
Negative example 2
Create a concurrent function decorated with @Concurrent, and execute the resolve() method asynchronously through a promise.
// Sample6.ets
@Concurrent
async function returnPromise() {
let promise = new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
})
return promise;
} In the onClick() method of the component, execute the returnPromise() method through TaskPool and use try...catch... to capture the exception.
// Sample6.ets
taskpool.execute(returnPromise).catch((e: BusinessError) => {
hilog.error(0xFF00, 'sampleTag', "error info: " + e.message);
}) In negative example 2, a promise in the pending state is returned, which is currently intercepted. The promise operation needs to be completed in the current thread.
According to the ECMA specifications, when methods and variables are imported, the JavaScript runtime performs a deep traversal of each file on the import chain starting from the entry file you specified. Files at leaf nodes are executed first. Each file runs only once and is then stored in the thread cache. Subsequent loads can directly obtain the file.
For concurrentFunc() in TaskPool, although the precise import mechanism is enabled, only the modules required for the current execution are imported at the top level of concurrentFunc(). However, no system-side optimization can be applied for large-sized modules or other modules imported as dependencies within the current module.
import { a } from './a';
import { b } from './b';
import { c } from './c';
@Concurrent
function concurrentFunc() {
hilog.info(0xFF00, 'sampleTag', 'value: ' + a);
} In the preceding example, although modules a, b, and c are all referenced, only module a is used in concurrentFunc(). Therefore, when concurrentFunc() is executed in TaskPool, only module a is parsed and executed. However, all modules on which module a depends are still parsed and executed.
Another common scenario is HAR importing. When import { case } from '@ohos/common' is used, the frontend translates it into import { case } from '@ohos/common/index'. The index file serves as the entry that exports all APIs of the entire HAR. Although you may want to use only one API, the entire HAR (including the files imported into the HAR) is actually executed. As a result, the execution takes a long time. Although this overhead occurs in the child thread, it also has a great impact on the task execution time and memory.
Generally, after the HAR is imported in a child thread, only certain independent methods in the HAR are used. In this case, precise import can bring significant benefits. For example, use import { case } from '@ohos/common/xxx/xxx/case' to import a specific file (with operability considered), or encapsulate this method into a new file. In this way, only this separate file is parsed.
Therefore, when using TaskPool, you should follow the minimum import principle, especially avoiding importing a large number of irrelevant files or HARs in independent tasks.
The context objects in different threads are different. Therefore, TaskPool worker threads can use only thread-safe modules. For example, UI-related non-thread-safe modules cannot be used. Worker threads such as those created by TaskPool and Worker do not support UI-related modules, non-thread-safe modules, or modules that can be used only in the main thread. UI-related modules are unsupported because worker threads are not allowed to operate on the UI. Non-thread-safe modules are unsupported because concurrent access from multiple threads may introduce multithreading issues. Modules that can be used only in the main thread, such as ApplicationContext, are explicitly described in the documentation. A thread-safe module refers to a module that can be accessed concurrently from multiple threads without causing multithreading issues, such as TaskPool, Worker, and hilog.
The maximum number of threads in the TaskPool thread pool is limited. The current policy sets maxThreads to the number of CPU cores minus one. Therefore, the maximum number of threads is 11 for current devices. When all threads are occupied, subsequent tasks may not be scheduled in a timely manner. Therefore, you are advised to use asynchronous APIs rather than synchronous APIs inside worker threads. This allows new tasks to be processed in a timely manner when waiting for I/O operations, preventing the task pool from being blocked.
The following uses copy APIs provided by the file system module as an example to compare the synchronous and asynchronous APIs. File copy is time-consuming. Therefore, you are advised to use the asynchronous API in a child thread.
Synchronous:
let srcPath = pathDir + "/srcDir/test.txt"; let dstPath = pathDir + "/dstDir/test.txt"; fileIo.copyFileSync(srcPath, dstPath);
Asynchronous:
import { BusinessError } from '@kit.BasicServicesKit';
let srcPath = pathDir + "/srcDir/test.txt";
let dstPath = pathDir + "/dstDir/test.txt";
fileIo.copyFile(srcPath, dstPath, 0).then(() => {
hilog.info(0xFF00, 'sampleTag', 'copy file succeed');
}).catch((err: BusinessError) => {
hilog.error(0xFF00, 'sampleTag',
'copy file failed with error message: ' + err.message + ', error code: ' + err.code);
}); By default, Sendable data is allocated in the shared heap, and data is passed between concurrent ArkTS instances through references. Unlike deep copy transmission, this approach delivers a noticeable reduction in serialization and deserialization time on traces, making it ideal for performance-sensitive scenarios.
For example, when obtaining data from a network database, you can offload the data obtaining logic to a child thread and declare the desired service model class as Sendable. After the child thread obtains JSON data, converts it, and generates model data, the Sendable-marked data is passed back to the main thread by reference, significantly reducing the load on the main thread.
In addition, since child threads do not support UI attributes, the data placed in child threads must be separated from the UI-related content. The returned data must be properly combined with the UI and rendered. For details about the combination of Sendable objects and UI, see the new ArkUI feature makeObserved. The makeObserved and @Sendable can be used together to meet the requirements of ViewModel display and data observation in the UI thread when big data processing is performed in child threads during application development.
In addition, the Sendable feature supports shared modules. For some singleton utility classes, due to thread isolation, they need to be reinitialized using Init when called in a child thread. After the shared module is used, it can be ensured that multiple threads share a singleton, optimizing the code and improving usability. For some time-consuming SDKs, if shared modules can be used, the initialization procedure can be moved to a child thread. When the main thread uses the shared modules, the related modules do not need to be reinitialized, which effectively improves performance.
ArkTS execution is performed in a single thread, which means that asynchronous functions do not create their own threads. Even if TaskPool is used, tasks are executed in child threads of the task pool, but the result logic needs to be returned to the host thread of the tasks for processing.
Generally, after you call an API from an ArkTS thread for an asynchronous task, the corresponding API returns a promise and suspends the execution, while the asynchronous logic runs in the worker thread. After the asynchronous logic is executed, the result is returned to the ArkTS thread through the inter-thread communication mechanism. After the result is delivered back to the ArkTS thread, the corresponding trace event is marked as Napi complete. At this point, napi_resolve_deferred is called.
When the promise state changes to fulfilled, the microtask queue is executed, which means the logic defined by you after await or in then is executed. This execution time is closely related to the logic and quantity of microtasks, and is also included in the time counted by the Napi complete trace. This long duration is not caused by the system but by your own code. If the execution takes a long time, for example, complex processing on returned data in a forEach loop or UI rendering, frame loss may occur.